-
# frozen_string_literal: true
-
-
2
module AllocationRecordCard
-
2
class View < ViewComponent::Base
-
2
with_collection_parameter :allocation
-
2
delegate :provider, :accredited_body, to: :allocation
-
2
delegate :provider_name, to: :provider
-
-
2
attr_reader :allocation, :heading_level
-
-
2
def initialize(heading_level = 3, allocation:, allocation_iteration:)
-
16
super
-
16
@allocation = allocation
-
16
@heading_level = heading_level
-
16
@iteration = allocation_iteration
-
end
-
-
2
def provider_code
-
16
tag.p("Provider code: #{provider.provider_code}", class: "govuk-caption-m govuk-!-font-size-16 allocation-record-card__id govuk-!-margin-bottom-0")
-
end
-
-
2
def accredited_body_code
-
16
tag.p("Accrediting Body code: #{accredited_body&.provider_code}", class: "govuk-caption-m govuk-!-font-size-16 allocation-record-card__id govuk-!-margin-bottom-0")
-
end
-
-
2
def accredited_body_name
-
16
tag.p("Accredited by #{accredited_body&.provider_name}", class: "govuk-caption-m govuk-!-font-size-16 allocation-record-card__id govuk-!-margin-bottom-0")
-
end
-
-
2
def number_of_places
-
16
tag.p("Allocated places: #{allocation.confirmed_number_of_places}", class: "govuk-heading-m govuk-!-font-size-16 allocation-record-card__id govuk-!-margin-bottom-0")
-
end
-
-
2
def number_of_uplifts
-
16
tag.p("Allocation Uplifts: #{allocation.allocation_uplift&.uplifts.to_i}", class: "govuk-heading-m govuk-!-font-size-16 allocation-record-card__id govuk-!-margin-bottom-0")
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module DegreePreviewComponent
-
1
class View < ViewComponent::Base
-
1
include PublishHelper
-
-
1
attr_reader :course
-
-
1
def initialize(course:)
-
2
super
-
2
@course = course
-
end
-
-
1
private
-
-
1
def subject_name(course)
-
1
case course.subjects.count
-
when 1
-
1
course.subjects.first.subject_name
-
when 2
-
"#{course.subjects.first.subject_name} or #{course.subjects.last.subject_name}"
-
else
-
course.name
-
end
-
end
-
-
1
def degree_grade_content(course)
-
2
{
-
"two_one" => "An undergraduate degree at class 2:1 or above, or equivalent.",
-
"two_two" => "An undergraduate degree at class 2:2 or above, or equivalent.",
-
"third_class" => "An undergraduate degree, or equivalent. This should be an honours degree (Third or above), or equivalent.",
-
"not_required" => "An undergraduate degree, or equivalent.",
-
}[course.degree_grade]
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
4
module DegreeRowContentComponent
-
4
class View < ViewComponent::Base
-
4
attr_reader :course, :errors
-
-
4
DEGREE_GRADE_MAPPING = {
-
"two_one" => "2:1 or above, or equivalent",
-
"two_two" => "2:2 or above, or equivalent",
-
"third_class" => "Third class degree or above, or equivalent",
-
"not_required" => "An undergraduate degree, or equivalent",
-
}.freeze
-
-
4
def initialize(course:, errors: nil)
-
35
super
-
35
@course = course
-
35
@errors = errors
-
end
-
-
4
def inset_text_css_classes
-
4
messages = errors&.values&.flatten
-
-
4
if messages&.include?("Enter degree requirements")
-
"app-inset-text--narrow-border app-inset-text--error"
-
else
-
4
"app-inset-text--narrow-border app-inset-text--important"
-
end
-
end
-
-
4
def has_errors?
-
2
inset_text_css_classes.include?("app-inset-text--error")
-
end
-
-
4
private
-
-
4
def degree_grade_content(course)
-
33
DEGREE_GRADE_MAPPING[course.degree_grade]
-
end
-
end
-
end
-
3
module Filters
-
3
module AllocationAttributes
-
3
class View < ViewComponent::Base
-
3
attr_accessor :filters
-
-
3
def initialize(filters:)
-
9
@filters = filters
-
9
super
-
end
-
end
-
end
-
end
-
4
module Filters
-
4
module ProviderAttributes
-
4
class View < ViewComponent::Base
-
4
attr_accessor :filters
-
-
4
def initialize(filters:)
-
24
@filters = filters
-
24
super
-
end
-
end
-
end
-
end
-
3
module Filters
-
3
module UserAttributes
-
3
class View < ViewComponent::Base
-
3
attr_accessor :filters
-
-
3
def initialize(filters:)
-
17
@filters = filters
-
17
super
-
end
-
-
3
private
-
-
3
def checked?(filters, filter, value)
-
34
filters && filters[filter]&.include?(value)
-
end
-
-
3
def label_for(attribute, value)
-
34
I18n.t("components.filter.users.#{attribute.pluralize}.#{value}")
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
4
module Filters
-
4
class View < ViewComponent::Base
-
4
attr_accessor :filters, :filter_model
-
-
4
def initialize(filters:, filter_model:)
-
44
@filters = filters
-
44
@filter_model = filter_model
-
44
super
-
end
-
-
4
private
-
-
4
def tags_for_filter(filter, value)
-
17
[value].flatten.map do |v|
-
17
{ title: title_html(filter, v), remove_link: remove_select_tag_link(filter) }
-
end
-
end
-
-
4
def filter_attributes
-
44
"::Filters::#{filter_model}Attributes::View".constantize.new(filters: filters)
-
end
-
-
4
def filter_label(filter)
-
17
t("components.filter.#{filter_model.to_s.downcase.pluralize}.#{filter}")
-
end
-
-
4
def title_html(filter, value)
-
17
tag.span("Remove ", class: "govuk-visually-hidden") + value + tag.span(" #{filter.humanize.downcase} filter", class: "govuk-visually-hidden")
-
end
-
-
4
def remove_select_tag_link(filter)
-
46
new_filters = filters.reject { |f| f == filter }
-
17
new_filters.to_query.blank? ? nil : "?#{new_filters.to_query}"
-
end
-
-
4
def reload_path
-
16
send("support_#{filter_model.to_s.downcase.pluralize}_path".to_sym)
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
4
module FlashBanner
-
4
class View < GovukComponent::Base
-
4
attr_reader :flash
-
-
4
FLASH_TYPES = %w[success warning info].freeze
-
-
4
def initialize(flash:)
-
998
super(classes: classes, html_attributes: html_attributes)
-
998
@flash = flash
-
end
-
-
4
def display?
-
998
flash.any?
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module GcsePreviewComponent
-
1
class View < GcseRequirementsComponent::View
-
1
include PublishHelper
-
end
-
end
-
# frozen_string_literal: true
-
-
4
module GcseRequirementsComponent
-
4
class View < ViewComponent::Base
-
4
attr_reader :course, :errors
-
-
4
def initialize(course:, errors: nil)
-
42
super
-
42
@course = course
-
42
@errors = errors
-
end
-
-
4
def inset_text_css_classes
-
40
messages = errors&.values&.flatten
-
-
40
if messages&.include?("Enter GCSE requirements")
-
3
"app-inset-text--narrow-border app-inset-text--error"
-
else
-
37
"app-inset-text--narrow-border app-inset-text--important"
-
end
-
end
-
-
4
def has_errors?
-
19
inset_text_css_classes.include?("app-inset-text--error")
-
end
-
-
4
private
-
-
4
def required_gcse_summary_content(course)
-
19
case course.level
-
when "primary"
-
14
"Grade #{course.gcse_grade_required} (C) or above in English, maths and science, or equivalent qualification"
-
when "secondary"
-
3
"Grade #{course.gcse_grade_required} (C) or above in English and maths, or equivalent qualification"
-
end
-
end
-
-
4
def required_gcse_content(course)
-
case course.level
-
when "primary"
-
"GCSE grade #{course.gcse_grade_required} (C) or above in English, maths and science, or equivalent qualification."
-
when "secondary"
-
"GCSE grade #{course.gcse_grade_required} (C) or above in English and maths, or equivalent qualification."
-
end
-
end
-
-
4
def pending_gcse_summary_content(course)
-
19
if course.accept_pending_gcse
-
14
"Candidates with pending GCSEs will be considered"
-
else
-
5
"Candidates with pending GCSEs will not be considered"
-
end
-
end
-
-
4
def pending_gcse_content(course)
-
if course.accept_pending_gcse
-
"We’ll consider candidates who are currently taking GCSEs."
-
else
-
"We will not consider candidates with pending GCSEs."
-
end
-
end
-
-
4
def gcse_equivalency_summary_content(course)
-
19
if course.accept_gcse_equivalency
-
11
"Equivalency tests will be accepted in #{equivalencies}."
-
else
-
8
"Equivalency tests will not be accepted"
-
end
-
end
-
-
4
def gcse_equivalency_content(course)
-
if course.accept_gcse_equivalency
-
"We’ll consider candidates who need to take a GCSE equivalency test in #{equivalencies}."
-
else
-
"We will not consider candidates who need to take GCSE equivalency tests."
-
end
-
end
-
-
4
def equivalencies
-
11
subjects = []
-
11
subjects << "English" if course.accept_english_gcse_equivalency.present?
-
11
subjects << "maths" if course.accept_maths_gcse_equivalency.present?
-
11
subjects << "science" if course.accept_science_gcse_equivalency.present?
-
-
11
subjects.to_sentence(last_word_connector: " or ", two_words_connector: " or ")
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
4
module GcseRowContentComponent
-
4
class View < GcseRequirementsComponent::View
-
end
-
end
-
# frozen_string_literal: true
-
-
4
class Header::View < GovukComponent::Base
-
4
attr_reader :service_name, :current_user
-
-
4
include ActiveModel
-
-
4
def initialize(service_name:, current_user: nil)
-
998
super(classes: classes, html_attributes: html_attributes)
-
998
@service_name = service_name
-
998
@current_user = current_user
-
end
-
-
4
def environment_header_class
-
998
"app-header--#{Settings.environment.name}"
-
end
-
end
-
# frozen_string_literal: true
-
-
3
class NavigationBar::View < GovukComponent::Base
-
3
attr_reader :items, :current_path
-
-
3
def initialize(items:, current_path:, current_user: {})
-
14
super(classes: classes, html_attributes: html_attributes)
-
14
@items = items
-
14
@current_path = current_path
-
14
@current_user = current_user
-
end
-
-
3
def item_link(item)
-
38
link_params = { class: "moj-primary-navigation__link" }
-
38
link_params.merge!(aria: { current: "page" }) if show_current_link?(item)
-
38
govuk_link_to(item[:name], item[:url], link_params)
-
end
-
-
3
def user_signed_in?
-
14
@current_user.present?
-
end
-
-
3
private
-
-
3
def show_current_link?(item)
-
78
item.fetch(:current, false) || [item.fetch(:url), item[:additional_url]].compact.any? { |url| current_path.include?(url) }
-
end
-
end
-
# frozen_string_literal: true
-
-
4
module NotificationBanner
-
4
class View < GovukComponent::Base
-
4
attr_reader :text, :title_id
-
-
4
SUCCESS_TITLE = "Success"
-
4
DEFAULT_TITLE = "Important"
-
-
4
SUCCESS_ROLE = "alert"
-
4
DEFAULT_ROLE = "region"
-
-
4
DEFAULT_CLASS = "govuk-notification-banner"
-
-
4
def initialize(
-
title_text: nil,
-
text: nil,
-
type: nil,
-
classes: [],
-
role: nil,
-
title_id: nil,
-
disable_auto_focus: false,
-
html_attributes: nil
-
)
-
72
super(classes: classes, html_attributes: html_attributes)
-
72
@title_text = title_text
-
72
@text = text
-
72
@type = type
-
72
@role = role
-
72
@title_id = title_id || "#{DEFAULT_CLASS}-title"
-
72
@disable_auto_focus = disable_auto_focus
-
end
-
-
4
private
-
-
4
attr_reader :title_text, :role, :type, :disable_auto_focus
-
-
4
def default_classes
-
72
[DEFAULT_CLASS]
-
end
-
-
4
def type_class
-
72
"#{DEFAULT_CLASS}--#{type}" if success_banner?
-
end
-
-
4
def title
-
72
return title_text if title_text
-
71
return SUCCESS_TITLE if success_banner?
-
-
16
DEFAULT_TITLE
-
end
-
-
4
def role_attribute
-
72
return role if role
-
# If type is success, add `role="alert"` to prioritise the information in
-
# the notification banner to users of assistive technologies
-
71
return SUCCESS_ROLE if success_banner?
-
-
# Otherwise add `role="region"` to make the notification banner a landmark
-
# to help users of assistive technologies to navigate to the banner
-
16
DEFAULT_ROLE
-
end
-
-
4
def data_attributes
-
72
attrs = { module: DEFAULT_CLASS }
-
72
attrs[:disable_auto_focus] = disable_auto_focus if disable_auto_focus
-
72
attrs
-
end
-
-
4
def success_banner?
-
214
type&.to_sym == :success
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
4
class PageTitle::View < GovukComponent::Base
-
4
I18N_FORMAT = /^\S*\.\S*$/.freeze
-
-
4
attr_accessor :title
-
-
4
def initialize(title: "", has_errors: false)
-
371
super(classes: classes, html_attributes: html_attributes)
-
371
@title = title
-
371
@has_errors = has_errors
-
end
-
-
4
def build_page_title
-
371
[build_error + build_title + build_service_name, "GOV.UK"].join(" - ")
-
end
-
-
4
private
-
-
4
attr_reader :has_errors
-
-
4
def build_error
-
371
has_errors ? "Error: " : ""
-
end
-
-
4
def build_title
-
371
title.match?(I18N_FORMAT) ? I18n.t("components.page_titles.#{title}") : title
-
end
-
-
4
def build_service_name
-
371
title.present? ? " - #{I18n.t('service_name')}" : I18n.t("service_name")
-
end
-
end
-
# frozen_string_literal: true
-
-
4
module PaginatedFilter
-
4
class View < ViewComponent::Base
-
4
attr_reader :filter_params, :collection
-
-
4
def initialize(filter_params:, collection:)
-
42
super
-
42
@filter_params = filter_params
-
42
@collection = collection
-
end
-
-
4
def filters
-
42
filter_params.slice(*allowed_search_params_keys)
-
end
-
-
4
private
-
-
4
def allowed_search_params_keys
-
{
-
42
User: %i[text_search user_type],
-
Provider: %i[provider_search course_search],
-
Allocation: [:text_search],
-
}[collection.klass.name.to_sym]
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
4
module Paginator
-
4
class View < ViewComponent::Base
-
4
attr_reader :scope
-
-
# This constant limits the number of links to pages rendered by #paginate.
-
# In some cases, the number of links will be KAMINARI_LINKS_LIMIT + 1 because
-
# kaminari refuses to leave a gap of just one number e.g. 1 ... 345 ... 9
-
# so it renders 1 2 3 4 5 ... 9 instead.
-
4
KAMINARI_LINKS_LIMIT = 5
-
-
4
def initialize(scope:)
-
83
super
-
83
@scope = scope
-
end
-
-
4
def render?
-
66
scope.total_pages > 1
-
end
-
-
4
def page_start
-
5
((scope.current_page - 1) * scope.limit_value) + 1
-
end
-
-
4
def page_end
-
5
[scope.current_page * scope.limit_value, total].min
-
end
-
-
4
def total
-
10
scope.total_count
-
end
-
-
4
def paginate_configuration
-
{
-
22
window: numbers_each_side,
-
left: numbers_left,
-
right: numbers_right,
-
}
-
end
-
-
4
private
-
-
4
def total_pages_exceed_limit?
-
66
scope.total_pages > KAMINARI_LINKS_LIMIT
-
end
-
-
4
def numbers_each_side
-
22
return 4 unless total_pages_exceed_limit?
-
-
12
if scope.current_page >= (KAMINARI_LINKS_LIMIT - 1) &&
-
9
scope.current_page <= scope.total_pages - (KAMINARI_LINKS_LIMIT - 2)
-
-
6
((KAMINARI_LINKS_LIMIT - 2).to_f / 2).floor
-
else
-
6
0
-
end
-
end
-
-
4
def numbers_left
-
22
return 0 unless total_pages_exceed_limit?
-
-
12
if scope.current_page < (KAMINARI_LINKS_LIMIT - 1)
-
3
KAMINARI_LINKS_LIMIT - 1
-
else
-
9
1
-
end
-
end
-
-
4
def numbers_right
-
22
return 0 unless total_pages_exceed_limit?
-
-
12
if scope.current_page > scope.total_pages - (KAMINARI_LINKS_LIMIT - 2)
-
3
KAMINARI_LINKS_LIMIT - 1
-
else
-
9
1
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
2
module Persona
-
2
class View < GovukComponent::Base
-
2
attr_accessor :email_address, :first_name, :last_name
-
-
2
def initialize(
-
email_address:,
-
first_name:,
-
last_name:,
-
classes: [],
-
html_attributes: nil
-
)
-
4
super(classes: classes, html_attributes: html_attributes)
-
4
@email_address = email_address
-
4
@first_name = first_name
-
4
@last_name = last_name
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
4
class PhaseBanner::View < ViewComponent::Base
-
4
def initialize(no_border: false)
-
1001
super
-
1001
@no_border = no_border
-
end
-
-
4
def environment_label
-
1001
Settings.environment.label
-
end
-
-
4
def environment_colour
-
1001
{
-
"development" => "grey",
-
"qa" => "orange",
-
"review" => "purple",
-
"rollover" => "turquoise",
-
"sandbox" => "purple",
-
"staging" => "red",
-
"unknown-environment" => "yellow",
-
}[Settings.environment.name]
-
end
-
-
4
def sandbox_mode?
-
1001
Settings.environment.name == "sandbox"
-
end
-
-
4
def feedback_link_to
-
999
Settings.feedback.link
-
end
-
end
-
4
module Providers
-
4
module ProviderList
-
4
class View < GovukComponent::Base
-
4
include Support::TimeHelper
-
-
4
def initialize(provider:)
-
17
super(classes: classes, html_attributes: html_attributes)
-
17
@provider = provider
-
end
-
-
4
def formatted_provider_type
-
14
Provider.human_attribute_name(@provider.provider_type)
-
end
-
-
4
def formatted_accrediting_provider
-
14
@provider.accredited_body? ? "Yes" : "No"
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
4
module TabNavigation
-
4
class View < GovukComponent::Base
-
4
attr_reader :items
-
-
4
def initialize(items:)
-
77
super(classes: classes, html_attributes: html_attributes)
-
77
@items = items
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
3
module TitleBar
-
3
class View < ViewComponent::Base
-
3
attr_accessor :title
-
-
3
def initialize(title:)
-
5
super
-
5
@title = title
-
end
-
-
3
def link
-
5
govuk_link_to t("change_organisation"), root_path, class: "title-bar-link inline govuk-link--no-visited-state"
-
end
-
end
-
end
-
4
module API
-
4
module Public
-
4
module V1
-
4
class ApplicationController < PublicAPIController
-
4
include PagyPagination
-
end
-
end
-
end
-
end
-
1
module API
-
1
module Public
-
1
module V1
-
1
class CoursesController < API::Public::V1::ApplicationController
-
1
def index
-
16
render jsonapi: paginate(courses),
-
include: include_param,
-
meta: { count: courses.count("course.id") },
-
class: API::Public::V1::SerializerService.call
-
rescue ActiveRecord::StatementInvalid
-
1
render json: { status: 400, message: "Invalid changed_since value, the format should be an ISO8601 UTC timestamp, for example: `2019-01-01T12:01:00Z`" }.to_json, status: :bad_request
-
end
-
-
1
private
-
-
1
def courses
-
28
@courses ||= CourseSearchService.call(
-
filter: params[:filter],
-
sort: params[:sort],
-
course_scope: recruitment_cycle.courses,
-
)
-
end
-
-
1
def recruitment_cycle
-
16
@recruitment_cycle = RecruitmentCycle.find_by(
-
year: params[:recruitment_cycle_year],
-
) || RecruitmentCycle.current_recruitment_cycle
-
end
-
-
1
def include_param
-
12
params.fetch(:include, "")
-
end
-
end
-
end
-
end
-
end
-
2
module API
-
2
module Public
-
2
module V1
-
2
class ProviderSuggestionsController < API::Public::V1::ApplicationController
-
2
def index
-
3
return render_json_error(status: 400, message: I18n.t("provider_suggestion.errors.bad_request")) if invalid_query?
-
-
2
found_providers = recruitment_cycle.providers
-
.with_findable_courses
-
.provider_search(params[:query])
-
.limit(10)
-
-
2
render(
-
jsonapi: found_providers,
-
class: { Provider: SerializableProviderSuggestion },
-
)
-
end
-
-
2
private
-
-
2
def recruitment_cycle
-
2
@recruitment_cycle = RecruitmentCycle.find_by(
-
year: params[:recruitment_cycle_year],
-
) || RecruitmentCycle.current_recruitment_cycle
-
end
-
-
2
def invalid_query?
-
3
params[:query].nil? || params[:query].length < 3
-
end
-
end
-
end
-
end
-
end
-
2
module API
-
2
module Public
-
2
module V1
-
2
module Providers
-
2
module Courses
-
2
class LocationsController < API::Public::V1::ApplicationController
-
2
def index
-
5
render jsonapi: locations,
-
include: include_param,
-
expose: { course: course, location_statuses: location_statuses },
-
class: API::Public::V1::SerializerService.call
-
end
-
-
2
private
-
-
2
def locations
-
5
@locations ||= course&.sites
-
end
-
-
2
def location_statuses
-
5
@location_statuses ||= course&.site_statuses
-
end
-
-
2
def course
-
13
@course ||= provider.courses.includes(site_statuses: [:site]).find_by(course_code: params[:course_code])
-
end
-
-
2
def provider
-
4
@provider ||= recruitment_cycle.providers.find_by(provider_code: params[:provider_code])
-
end
-
-
2
def recruitment_cycle
-
4
@recruitment_cycle ||= RecruitmentCycle.find_by(year: params[:recruitment_cycle_year])
-
end
-
-
2
def include_param
-
5
params.fetch(:include, "")
-
end
-
end
-
end
-
end
-
end
-
end
-
end
-
2
module API
-
2
module Public
-
2
module V1
-
2
module Providers
-
2
class CoursesController < API::Public::V1::ApplicationController
-
2
def index
-
12
render jsonapi: paginate(courses),
-
include: include_param,
-
class: API::Public::V1::SerializerService.call
-
end
-
-
2
def show
-
5
render jsonapi: course,
-
include: include_param,
-
class: API::Public::V1::SerializerService.call
-
end
-
-
2
private
-
-
2
def courses
-
12
@courses ||= CourseSearchService.call(filter: params[:filter],
-
course_scope: provider.courses)
-
end
-
-
2
def course
-
5
@course ||= provider.courses.find_by!(course_code: params[:code])
-
end
-
-
2
def provider
-
17
@provider ||= recruitment_cycle.providers.find_by!(provider_code: params[:provider_code])
-
end
-
-
2
def recruitment_cycle
-
17
@recruitment_cycle ||= RecruitmentCycle.find_by(year: params[:recruitment_cycle_year])
-
end
-
-
2
def include_param
-
12
params.fetch(:include, "")
-
end
-
end
-
end
-
end
-
end
-
end
-
2
module API
-
2
module Public
-
2
module V1
-
2
module Providers
-
2
class LocationsController < API::Public::V1::ApplicationController
-
2
def index
-
5
render jsonapi: locations,
-
include: include_param,
-
class: API::Public::V1::SerializerService.call
-
end
-
-
2
private
-
-
2
def locations
-
5
@locations ||= provider&.sites
-
end
-
-
2
def provider
-
4
@provider ||= recruitment_cycle.providers.find_by(provider_code: params[:provider_code])
-
end
-
-
2
def recruitment_cycle
-
4
@recruitment_cycle ||= RecruitmentCycle.find_by(year: params[:recruitment_cycle_year])
-
end
-
-
2
def include_param
-
5
params.fetch(:include, "")
-
end
-
end
-
end
-
end
-
end
-
end
-
2
module API
-
2
module Public
-
2
module V1
-
2
class ProvidersController < API::Public::V1::ApplicationController
-
2
def index
-
19
render jsonapi: paginate(providers),
-
include: params[:include], class: API::Public::V1::SerializerService.call, fields: fields
-
end
-
-
2
def show
-
8
code = params.fetch(:code, params[:provider_code])
-
8
provider = recruitment_cycle.providers
-
.find_by!(
-
provider_code: code.upcase,
-
)
-
-
5
render jsonapi: provider,
-
class: API::Public::V1::SerializerService.call,
-
include: params[:include],
-
fields: fields
-
end
-
-
2
private
-
-
2
def updated_since
-
20
@updated_since ||= params.dig(:filter, :updated_since)
-
end
-
-
2
def providers
-
19
@providers = recruitment_cycle.providers
-
19
@providers = if sort_by_provider_ascending?
-
18
@providers.by_name_ascending
-
else
-
1
@providers.by_name_descending
-
end
-
-
19
if updated_since.present?
-
1
@providers = @providers.changed_since(updated_since)
-
end
-
-
19
@providers
-
end
-
-
2
def recruitment_cycle
-
27
@recruitment_cycle = RecruitmentCycle.find_by(
-
year: params[:recruitment_cycle_year],
-
) || RecruitmentCycle.current_recruitment_cycle
-
end
-
-
2
def fields
-
21
{ providers: provider_fields } if provider_fields.present?
-
end
-
-
2
def sort_by_provider_ascending?
-
19
sort_field.include?("name") || !sort_by_provider_descending?
-
end
-
-
2
def sort_by_provider_descending?
-
16
sort_field.include?("-name")
-
end
-
-
2
def sort_field
-
35
@sort_field ||= Set.new(params[:sort]&.split(","))
-
end
-
-
2
def provider_fields
-
22
params.dig(:fields, :providers)&.split(",")
-
end
-
end
-
end
-
end
-
end
-
2
module API
-
2
module Public
-
2
module V1
-
2
class SubjectAreasController < API::Public::V1::ApplicationController
-
2
def index
-
5
render(
-
jsonapi: SubjectArea.active.includes(subjects: [:financial_incentive]),
-
include: params[:include],
-
class: API::Public::V1::SerializerService.call,
-
)
-
end
-
end
-
end
-
end
-
end
-
1
module API
-
1
module Public
-
1
module V1
-
1
class SubjectsController < API::Public::V1::ApplicationController
-
1
def index
-
3
subjects = Subject.active.includes(:financial_incentive)
-
-
3
if params["sort"] == "name"
-
2
subjects = subjects.order(:subject_name)
-
end
-
3
render jsonapi: subjects,
-
class: API::Public::V1::SerializerService.call,
-
include: params[:include],
-
fields: fields
-
end
-
-
1
def fields
-
3
{ subjects: subject_fields } if subject_fields.present?
-
end
-
-
1
def subject_fields
-
4
params.dig(:fields, :subjects)&.split(",")
-
end
-
end
-
end
-
end
-
end
-
1
module API
-
1
module System
-
1
class ApplicationController < ::ApplicationController
-
1
before_action -> { skip_authorization }
-
-
1
def authenticate
-
2
authenticate_or_request_with_http_token do |token|
-
2
ActiveSupport::SecurityUtils.secure_compare(
-
token,
-
Settings.system_authentication_token,
-
)
-
end
-
end
-
end
-
end
-
end
-
2
module API
-
2
module V3
-
2
class AccreditedProviderTrainingProvidersController < API::V3::ApplicationController
-
2
before_action :build_recruitment_cycle
-
2
before_action :build_provider
-
-
2
class TrainingProviderSearch
-
2
attr_reader :provider, :params
-
-
2
def initialize(provider:, params:)
-
30
@provider = provider
-
30
@params = params
-
end
-
-
2
def call
-
30
scope = all_providers.order(:provider_name)
-
-
30
if params[:filter]
-
16
scope = scope.where(id: eligible_training_provider_ids)
-
end
-
-
30
scope
-
end
-
-
2
private
-
-
2
def course_scope
-
16
provider_courses = Course.where(provider: [provider.id])
-
16
training_provider_courses = Course.where(provider: training_providers).where(accredited_body_code: provider.provider_code)
-
-
16
provider_courses.or(training_provider_courses).findable
-
end
-
-
2
def eligible_training_provider_ids
-
16
CourseSearchService.call(filter: params[:filter], course_scope: course_scope)
-
.pluck(:provider_id)
-
end
-
-
2
def all_providers
-
30
provider.training_providers.or(Provider.where(id: provider.id))
-
end
-
-
2
def training_providers
-
16
provider.training_providers
-
end
-
end
-
-
2
def index
-
authorize @provider, :can_list_training_providers?
-
-
providers = TrainingProviderSearch.new(provider: @provider, params: params)
-
.call
-
.include_accredited_courses_counts(@provider.provider_code)
-
-
accredited_courses_counts = {}
-
-
providers.each do |p|
-
accredited_courses_counts[p.provider_code] = p.accredited_courses_count
-
end
-
-
render jsonapi: providers,
-
include: params[:include],
-
meta: {
-
accredited_courses_counts: accredited_courses_counts,
-
}
-
end
-
-
2
def show
-
1
training_provider = @provider.training_providers.find_by(provider_code: params[:training_provider_code])
-
-
1
authorize training_provider, :can_show_training_provider?
-
-
1
render jsonapi: training_provider
-
end
-
-
2
private
-
-
2
def build_recruitment_cycle
-
1
@recruitment_cycle = RecruitmentCycle.find_by(
-
year: params[:recruitment_cycle_year],
-
) || RecruitmentCycle.current_recruitment_cycle
-
end
-
-
2
def build_provider
-
1
@provider = @recruitment_cycle.providers.find_by!(
-
provider_code: params[:provider_code].upcase,
-
)
-
end
-
end
-
end
-
end
-
4
module API
-
4
module V3
-
4
class ApplicationController < PublicAPIController
-
4
attr_reader :current_user
-
-
4
rescue_from ActiveRecord::RecordNotFound, with: :jsonapi_404
-
-
4
before_action :store_request_id
-
-
4
def authenticate
-
authenticate_or_request_with_http_token do |token|
-
@current_user = AuthenticationService.new(logger: Rails.logger).execute(token)
-
assign_sentry_contexts
-
@current_user.present?
-
end
-
end
-
-
4
def jsonapi_404
-
3
render jsonapi: nil, status: :not_found
-
end
-
-
4
private
-
-
4
def paginate(scope)
-
54
_pagy, paginated_records = pagy(scope, items: per_page, page: page)
-
-
53
paginated_records
-
end
-
-
4
def per_page
-
54
params[:page] ||= {}
-
-
54
[(params.dig(:page, :per_page) || default_per_page).to_i, max_per_page].min
-
end
-
-
4
def default_per_page
-
50
100
-
end
-
-
4
def max_per_page
-
46
100
-
end
-
-
4
def page
-
54
params[:page] ||= {}
-
54
(params.dig(:page, :page) || 1).to_i
-
end
-
-
4
def build_recruitment_cycle
-
89
@recruitment_cycle = RecruitmentCycle.find_by(
-
year: params[:recruitment_cycle_year],
-
) || RecruitmentCycle.current_recruitment_cycle
-
end
-
-
4
def fields_param
-
59
params.fetch(:fields, {})
-
.permit(:subject_areas, :subjects, :courses, :providers, :site_statuses)
-
.to_h
-
6
.map { |k, v| [k, v.split(",").map(&:to_sym)] }
-
end
-
-
4
def store_request_id
-
99
RequestStore.store[:request_id] = request.uuid
-
end
-
end
-
end
-
end
-
2
module API
-
2
module V3
-
2
class CoursesController < API::V3::ApplicationController
-
2
before_action :build_recruitment_cycle
-
2
before_action :build_provider
-
2
before_action :build_courses
-
-
2
def index
-
51
course_search = ::V3::CourseSearchService.call(filter: params[:filter], sort: params[:sort], course_scope: @courses)
-
-
51
render jsonapi: paginate(course_search),
-
fields: fields_param,
-
include: params[:include],
-
meta: { count: course_search.size },
-
class: CourseSerializersService.new(provider_serializer: API::V3::SerializableProvider).execute,
-
cache: Rails.cache
-
end
-
-
2
def show
-
5
@course = @courses.find_by!(course_code: params[:code].upcase)
-
-
5
if @course.is_published?
-
# https://github.com/jsonapi-rb/jsonapi-rails/issues/113
-
3
render jsonapi: @course,
-
fields: fields_param,
-
include: params[:include],
-
class: CourseSerializersService.new.execute
-
else
-
2
raise ActiveRecord::RecordNotFound
-
end
-
end
-
-
2
private
-
-
2
def max_per_page
-
48
if fields_for_sitemap?
-
2
20_000
-
else
-
46
super
-
end
-
end
-
-
2
def fields_for_sitemap?
-
48
if (courses = params.dig(:fields, :courses))
-
2
(courses.split(",") & %w[course_code provider_code changed_at]).size == 3
-
end
-
end
-
-
2
def build_courses
-
56
courses_base = @provider.present? ? @provider.courses : @recruitment_cycle.courses
-
-
56
@courses = courses_base.includes(
-
:enrichments,
-
subjects: [:financial_incentive],
-
site_statuses: [:site],
-
provider: %i[recruitment_cycle ucas_preferences],
-
).findable
-
end
-
-
2
def build_provider
-
57
if params[:provider_code].present?
-
11
@provider = @recruitment_cycle.providers.find_by!(
-
provider_code: params[:provider_code].upcase,
-
)
-
end
-
end
-
end
-
end
-
end
-
1
module API
-
1
module V3
-
1
class ProviderSuggestionsController < API::V3::ApplicationController
-
1
before_action :build_recruitment_cycle
-
-
1
def index
-
7
return render(status: :bad_request) if params[:query].nil? || params[:query].length < 3
-
-
5
found_providers = @recruitment_cycle.providers
-
.with_findable_courses
-
.provider_search(params[:query])
-
.limit(10)
-
-
5
render(
-
jsonapi: found_providers,
-
class: { Provider: SerializableProvider },
-
)
-
end
-
-
1
private
-
-
1
def begins_with_alphanumeric(string)
-
string.match?(/^[[:alnum:]].*$/)
-
end
-
end
-
end
-
end
-
2
module API
-
2
module V3
-
2
class ProvidersController < API::V3::ApplicationController
-
2
before_action :build_recruitment_cycle
-
-
2
def index
-
16
if params[:search].present? && (params[:search].length < 2)
-
1
return render(status: :bad_request)
-
end
-
-
15
build_fields_for_index
-
15
@providers = @recruitment_cycle.providers.includes(:recruitment_cycle)
-
15
@providers = @providers.provider_search(params[:search]) if params[:search].present?
-
-
15
render jsonapi: @providers.by_name_ascending, class: { Provider: API::V3::SerializableProvider }, fields: @fields
-
end
-
-
2
def show
-
9
code = params.fetch(:code, params[:provider_code])
-
9
@provider = @recruitment_cycle.providers
-
.includes(:sites, :courses, courses: [:enrichments, :sites, { site_statuses: [:site], provider: [:recruitment_cycle], subjects: [:financial_incentive] }])
-
.find_by!(
-
provider_code: code.upcase,
-
)
-
-
9
render jsonapi: @provider,
-
class: CourseSerializersService.new.execute,
-
include: params[:include],
-
fields: { providers: %i[provider_code provider_name courses
-
recruitment_cycle_year address1 address2
-
address3 address4 postcode region_code
-
email website telephone train_with_us
-
train_with_disability sites
-
accredited_bodies accredited_body?
-
can_sponsor_student_visa
-
can_sponsor_skilled_worker_visa] }
-
end
-
-
2
private
-
-
2
def build_fields_for_index
-
15
@fields = default_fields_for_index
-
-
15
return if params[:fields].blank? || params[:fields][:providers].blank?
-
-
1
@fields[:providers] = params[:fields][:providers].split(",")
-
end
-
-
2
def default_fields_for_index
-
{
-
15
providers: %w[provider_name provider_code recruitment_cycle_year],
-
}
-
end
-
end
-
end
-
end
-
1
module API
-
1
module V3
-
1
class SubjectAreasController < API::V3::ApplicationController
-
1
def index
-
3
render jsonapi: SubjectArea.active.includes(subjects: [:financial_incentive]), fields: fields_param, include: params[:include], class: CourseSerializersService.new.execute
-
end
-
end
-
end
-
end
-
1
module API
-
1
module V3
-
1
class SubjectsController < API::V3::ApplicationController
-
1
def index
-
3
subjects = Subject.active
-
-
3
if params["sort"] == "subject_name"
-
1
subjects = subjects.order(:subject_name)
-
end
-
-
3
render jsonapi: subjects, fields: fields_param, include: params[:include], class: CourseSerializersService.new.execute
-
end
-
end
-
end
-
end
-
class APIController < ActionController::API
-
include EmitsRequestEvents
-
include ActionController::HttpAuthentication::Token::ControllerMethods
-
include Pundit::Authorization
-
-
# child must define authenticate method
-
before_action :authenticate
-
after_action :verify_authorized
-
end
-
1
class APIErrorController < ActionController::API
-
1
def error500
-
1
raise "This is an error that is triggered by the application when a user accesses the /error_500 route. If you are seeing this in logs, you can ignore it."
-
end
-
-
1
def error_nodb
-
raise PG::ConnectionBad, "This is an error that is triggered by the application when a user accesses the /error_nodb route. If you are seeing this in logs, you can ignore it."
-
end
-
end
-
4
class ApplicationController < ActionController::Base
-
4
include EmitsRequestEvents
-
4
include Authentication
-
-
4
helper_method :current_user
-
4
before_action :authenticate
-
-
4
include Pundit::Authorization
-
4
include Pagy::Backend
-
-
1508
before_action :enforce_basic_auth, if: -> { BasicAuthenticable.required? }
-
-
4
default_form_builder GOVUKDesignSystemFormBuilder::FormBuilder
-
-
4
private
-
-
4
def enforce_basic_auth
-
authenticate_or_request_with_http_basic do |username, password|
-
BasicAuthenticable.authenticate(username, password)
-
end
-
end
-
-
4
def render_not_found
-
render "errors/not_found", status: :not_found, formats: :html
-
end
-
end
-
1
class ComponentPreviewsController < ViewComponentsController
-
1
include Authentication
-
1
helper_method :current_user
-
end
-
4
module Authentication
-
4
def user_session
-
2298
@user_session ||= UserSession.load_from_session(session)
-
end
-
-
4
def current_user
-
8723
@current_user ||= User.find_by(email: user_session&.email)
-
end
-
-
4
def authenticated?
-
1022
current_user.present?
-
end
-
-
4
def authenticate
-
1022
if !authenticated?
-
5
session["post_dfe_sign_in_path"] = request.fullpath
-
5
redirect_to sign_in_path
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
# Enforces HTTP Basic Auth
-
4
module BasicAuthenticable
-
4
class << self
-
4
def required?
-
1504
Settings.basic_auth.enabled
-
end
-
-
4
def authenticate(username, password)
-
validate(username, password)
-
end
-
-
4
def validate(username, password)
-
utils.secure_compare(::Digest::SHA256.hexdigest(auth_username), ::Digest::SHA256.hexdigest(username)) &
-
utils.secure_compare(::Digest::SHA256.hexdigest(auth_password), ::Digest::SHA256.hexdigest(password))
-
end
-
-
4
def auth_username
-
@auth_username ||= Settings.basic_auth.username
-
end
-
-
4
def auth_password
-
@auth_password ||= Settings.basic_auth.password
-
end
-
-
4
def utils
-
ActiveSupport::SecurityUtils
-
end
-
end
-
end
-
4
module CopyCourseContent
-
4
extend ActiveSupport::Concern
-
-
4
private
-
-
4
def copy_content_check(fields)
-
22
fetch_course_list_to_copy_from
-
-
22
if params[:copy_from].present?
-
4
fetch_course_to_copy_from
-
4
@copied_fields = ::Courses::Copy.get_present_fields_in_source_course(fields, @source_course, @course)
-
end
-
end
-
-
4
def copy_boolean_check(fields)
-
15
fetch_course_list_to_copy_from
-
-
15
if params[:copy_from].present?
-
4
fetch_course_to_copy_from
-
4
@copied_fields = ::Courses::Copy.get_boolean_fields(fields, @source_course, @course)
-
end
-
end
-
-
4
def fetch_course_to_copy_from
-
8
@source_course = ::Courses::Fetch.by_code(
-
provider_code: params[:provider_code],
-
course_code: params[:copy_from],
-
)
-
end
-
-
4
def fetch_course_list_to_copy_from
-
37
@courses_by_accrediting_provider = ::Courses::Fetch.by_accrediting_provider(@provider)
-
37
@self_accredited_courses = @courses_by_accrediting_provider.delete(@provider.provider_name)
-
-
41
@courses_by_accrediting_provider&.transform_values! { |v| v.reject { |x| x.id == course.id } }
-
112
@self_accredited_courses = @self_accredited_courses&.reject { |c| c.id == course.id }
-
end
-
end
-
4
module EmitsRequestEvents
-
4
extend ActiveSupport::Concern
-
4
include ApplicationHelper
-
-
4
included do
-
8
after_action :trigger_request_event
-
end
-
-
4
def trigger_request_event
-
1648
if FeatureService.enabled?(:send_request_data_to_bigquery)
-
2
request_event = BigQuery::RequestEvent.new do |event|
-
2
event.with_request_details(request)
-
2
event.with_response_details(response)
-
2
if respond_to?(:current_user, true)
-
1
event.with_user(current_user)
-
end
-
end
-
-
2
SendEventToBigQueryJob.perform_later(request_event.as_json)
-
end
-
end
-
end
-
4
module ErrorHandlers
-
4
module Base
-
4
extend ActiveSupport::Concern
-
-
4
included do
-
5
rescue_from(StandardError) do |e|
-
3
raise e unless Settings.render_json_errors
-
-
2
Sentry.capture_exception(e)
-
2
render_json_error(status: 500)
-
end
-
-
10
rescue_from(ActiveRecord::RecordNotFound) { render_json_error(status: 404) }
-
end
-
-
4
private
-
-
4
def render_json_error(status:, resource: nil, message: nil)
-
18
if resource.nil?
-
18
render json: error_hash(status, message), status: status
-
else
-
render jsonapi_errors: resource.errors, status: status
-
end
-
end
-
-
4
def error_hash(status, message = nil)
-
{
-
18
errors: [
-
{
-
status: status,
-
title: I18n.t("errors.#{status}.title"),
-
detail: I18n.t("errors.#{status}.detail", message: message),
-
},
-
],
-
}
-
end
-
end
-
end
-
4
module ErrorHandlers
-
4
module Pagy
-
4
def self.included(base)
-
4
base.include(ErrorHandlers::Base)
-
-
4
base.class_eval do
-
4
rescue_from ::Pagy::OverflowError do |_exception|
-
10
render_json_error(status: 400, message: I18n.t("pagy.overflow"))
-
end
-
end
-
end
-
end
-
end
-
4
module PagyPagination
-
4
extend ActiveSupport::Concern
-
-
4
def jsonapi_links
-
65
pagy_results.present? ? pagination_links : {}
-
end
-
-
4
def paginate(scope)
-
47
@pagy_results ||= pagy(scope, items: per_page, page: page)
-
-
37
pagy_results.second
-
end
-
-
4
private
-
-
4
attr_accessor :pagy_results
-
-
4
def pagination_links
-
37
meta = pagy_metadata(pagy_results.first, absolute: true)
-
-
{
-
37
first: meta[:first_url],
-
last: meta[:last_url],
-
37
prev: meta[:prev].nil? ? nil : meta[:prev_url],
-
37
next: meta[:next].nil? ? nil : meta[:next_url],
-
}
-
end
-
-
4
def per_page
-
47
[(per_page_parameter || default_per_page).to_i, max_per_page].min
-
end
-
-
4
def default_per_page
-
29
100
-
end
-
-
4
def max_per_page
-
47
500
-
end
-
-
4
def page
-
47
(page_parameter || 1).to_i
-
end
-
-
4
def page_parameter
-
47
return params[:page][:page] if page_is_nested?
-
-
47
params[:page]
-
end
-
-
4
def per_page_parameter
-
47
return params[:page][:per_page] if page_is_nested?
-
-
47
params[:per_page]
-
end
-
-
4
def page_is_nested?
-
94
params[:page].is_a?(ActionController::Parameters)
-
end
-
end
-
4
module StudyModeVacancyMapper
-
4
extend ActiveSupport::Concern
-
-
4
def update_vac_status(study_mode, site_status)
-
case study_mode
-
when "full_time"
-
site_status.update(vac_status: :full_time_vacancies)
-
when "part_time"
-
site_status.update(vac_status: :part_time_vacancies)
-
when "full_time_or_part_time"
-
site_status.update(vac_status: :both_full_time_and_part_time_vacancies)
-
else
-
raise "Unexpected study mode #{study_mode}"
-
end
-
end
-
end
-
4
module SuccessMessage
-
4
extend ActiveSupport::Concern
-
-
4
def course_details_success_message(value)
-
6
raise TypeError unless value.is_a?(String)
-
-
6
flash[:success] = @course.is_published? ? I18n.t("success.value_published", value: value) : I18n.t("success.value_saved", value: value)
-
end
-
-
4
def course_description_success_message(value)
-
7
raise TypeError unless value.is_a?(String)
-
-
7
flash[:success] = @course.only_published? ? I18n.t("success.value_published", value: value) : I18n.t("success.value_saved", value: value)
-
end
-
end
-
module ValidateJsonapiType
-
extend ActiveSupport::Concern
-
-
def validate_jsonapi_type(params, type)
-
# jsonapi-rb doesn't appear to validate the type of data record that's
-
# been given us, so even though we say "require(:session)" it won't
-
# validate the type field of the data object that was passed through.
-
#
-
# So for now, we do this by hand.
-
sent_type = params[:_jsonapi][:data][:type]
-
unless sent_type == type
-
raise ActionController::BadRequest, "data type '#{sent_type}' did not match expected type '#{type}'"
-
end
-
end
-
end
-
class ErrorsController < ApplicationController
-
skip_before_action :authenticate
-
-
def not_found
-
render status: 404
-
end
-
-
def forbidden
-
render status: 403
-
end
-
-
def internal_server_error
-
render status: 500
-
end
-
end
-
1
require "sidekiq/api"
-
-
1
class HeartbeatController < ActionController::API
-
1
def ping
-
1
render body: "PONG"
-
end
-
-
1
def healthcheck
-
checks = {
-
9
database: database_alive?,
-
redis: redis_alive?,
-
sidekiq_processes: sidekiq_processes_checks,
-
}
-
-
9
status = checks.values.all? ? :ok : :service_unavailable
-
-
9
render status: status,
-
json: {
-
checks: checks,
-
}
-
end
-
-
1
def sha
-
1
render json: { sha: ENV.fetch("COMMIT_SHA", nil) }
-
end
-
-
1
private
-
-
1
def database_alive?
-
9
ActiveRecord::Base.connection.active?
-
rescue PG::ConnectionBad
-
false
-
end
-
-
1
def redis_alive?
-
9
Sidekiq.redis_info
-
7
true
-
rescue StandardError
-
2
false
-
end
-
-
1
def sidekiq_processes_checks
-
9
stats = Sidekiq::Stats.new
-
9
processes = Sidekiq::ProcessSet.new
-
-
# Iterate over each Sidekiq queue and ensure that there is a process
-
# running for it.
-
9
stats.queues.keys.all? do |queue|
-
18
processes.any? { |process| queue.in? process["queues"] }
-
end
-
rescue StandardError
-
false
-
end
-
end
-
1
if AuthenticationService.magic_link?
-
1
class MagicLinkSessionsController < ApplicationController
-
1
layout "application"
-
-
1
skip_before_action :authenticate
-
-
1
before_action :redirect_if_token_is_invalid
-
1
before_action :redirect_if_token_expired
-
-
1
def create
-
1
update_user
-
1
set_user_session
-
1
record_first_login
-
1
send_welcome_email
-
-
1
redirect_to root_path
-
end
-
-
1
private
-
-
1
def magic_link_params
-
3
params.permit(:email, :token)
-
end
-
-
1
def redirect_if_token_is_invalid
-
3
return unless params[:token] != user.magic_link_token
-
-
1
redirect_to root_path, flash: {
-
error: {
-
id: "publish-authentication-magic-link-form-email-field",
-
message: t("publish_authentication.magic_link.invalid_token"),
-
},
-
}
-
end
-
-
1
def redirect_if_token_expired
-
2
return unless magic_link_token_expired?
-
-
1
redirect_to root_path, flash: {
-
error: {
-
id: "publish-authentication-magic-link-form-email-field",
-
message: t("publish_authentication.magic_link.expired"),
-
},
-
}
-
end
-
-
1
def user
-
11
@user ||= User.find_by(email: magic_link_params[:email])
-
end
-
-
1
def magic_link_token_expired?
-
2
magic_link_token_age = Time.zone.now - user.magic_link_token_sent_at
-
-
2
magic_link_token_age > Settings.magic_link.max_token_age.seconds
-
end
-
-
1
def update_user
-
1
user.update!(
-
last_login_date_utc: Time.now.utc,
-
magic_link_token: nil,
-
magic_link_token_sent_at: nil,
-
)
-
end
-
-
1
def set_user_session
-
1
session["user"] = {
-
"email" => user.email,
-
"first_name" => user.first_name,
-
"last_name" => user.last_name,
-
"last_active_at" => Time.zone.now,
-
}
-
end
-
-
1
def record_first_login
-
1
RecordFirstLoginService.new.execute(current_user: user)
-
end
-
-
1
def send_welcome_email
-
1
SendWelcomeEmailService.call(current_user: user)
-
end
-
end
-
end
-
1
if AuthenticationService.magic_link?
-
1
class MagicLinksController < ApplicationController
-
1
layout "application"
-
-
1
skip_before_action :authenticate
-
-
1
def new
-
4
@magic_link_form = Publish::Authentication::MagicLinkForm.new
-
end
-
-
1
def create
-
2
@magic_link_form = Publish::Authentication::MagicLinkForm.new(email: magic_link_params[:email])
-
-
2
if @magic_link_form.submit
-
1
redirect_to magic_link_sent_path
-
else
-
1
render :new
-
end
-
end
-
-
1
def magic_link_sent; end
-
-
1
private
-
-
1
def magic_link_params
-
2
params.require(:publish_authentication_magic_link_form).permit(:email)
-
end
-
end
-
end
-
1
class PagesController < ApplicationController
-
1
skip_before_action :authenticate, only: %i[
-
accessibility
-
cookies
-
guidance
-
performance_dashboard
-
privacy
-
terms
-
]
-
-
1
def accessibility; end
-
-
1
def cookies; end
-
-
1
def guidance; end
-
-
1
def performance_dashboard
-
@performance_data = PerformanceDashboardService.call
-
end
-
-
1
def privacy; end
-
-
1
def terms; end
-
end
-
if AuthenticationService.persona?
-
class PersonasController < ApplicationController
-
layout "application"
-
-
skip_before_action :authenticate
-
-
def index; end
-
end
-
end
-
4
class PublicAPIController < ActionController::API
-
4
include EmitsRequestEvents
-
4
include Pagy::Backend
-
4
include ErrorHandlers::Pagy
-
4
include Pundit::Authorization
-
end
-
1
module Publish
-
1
class AccessRequestsController < PublishController
-
6
before_action -> { authorize(access_request) }, except: :index
-
-
1
def index
-
4
authorize(AccessRequest)
-
-
4
@access_requests = AccessRequest.requested.includes(:requester)
-
end
-
-
1
def approve
-
1
access_request.approved!
-
1
flash[:success] = "Successfully approved request"
-
1
redirect_to inform_publisher_publish_access_request_path
-
end
-
-
1
def inform_publisher; end
-
-
1
def confirm; end
-
-
1
def destroy
-
1
access_request.destroy
-
1
flash[:success] = "Successfully deleted the Access Request"
-
1
redirect_to publish_access_requests_path
-
end
-
-
1
private
-
-
1
def access_request
-
7
@access_request ||= AccessRequest.includes(:requester, requester: [:organisations]).find(params[:id])
-
end
-
end
-
end
-
2
module Publish
-
2
module Courses
-
2
class AccreditedBodyController < PublishController
-
2
before_action :build_course_params, only: :continue
-
2
include CourseBasicDetailConcern
-
-
2
def edit
-
build_provider
-
end
-
-
2
def continue
-
2
authorize(@provider, :can_create_course?)
-
-
2
code = course_params[:accredited_body_code]
-
2
query = @accredited_body
-
-
2
@errors = errors_for_search_query(code, query)
-
-
2
if @errors.present?
-
1
render :new
-
1
elsif other_selected_with_no_autocompleted_code?(code)
-
redirect_to(
-
search_new_publish_provider_recruitment_cycle_courses_accredited_body_path(
-
query: @accredited_body,
-
course: course_params,
-
),
-
)
-
else
-
1
params[:course][:accredited_body_code] = @autocompleted_provider_code if @autocompleted_provider_code.present?
-
1
super
-
end
-
end
-
-
2
def search_new
-
authorize(provider, :can_create_course?)
-
-
# These are not before_action hooks as they conflict with hooks
-
# defined within the CourseBasicDetailConcern and cannot be overridden
-
# without causing failures in other routes in this controller
-
build_new_course
-
build_provider
-
build_previous_course_creation_params
-
@query = params[:query]
-
@provider_suggestions = recruitment_cycle.providers.with_findable_courses.provider_search(@query).limit(10)
-
end
-
-
2
def update
-
build_provider
-
code = update_course_params[:accredited_body_code]
-
query = update_course_params[:accredited_body]
-
-
@errors = errors_for_search_query(code, query)
-
return render :edit if @errors.present?
-
-
if update_params[:accredited_body_code] == "other"
-
redirect_to_provider_search
-
elsif @course.update(update_params)
-
redirect_to_update_successful
-
else
-
@errors = @course.errors.messages
-
render :edit
-
end
-
end
-
-
2
def search
-
build_course
-
@query = params[:query]
-
@provider_suggestions = recruitment_cycle.providers.with_findable_courses.provider_search(@query).limit(10)
-
end
-
-
2
private
-
-
2
def build_provider
-
@provider = RecruitmentCycle.find_by(year: params[:recruitment_cycle_year])
-
.providers
-
.find_by(provider_code: params[:provider_code])
-
end
-
-
2
def error_keys
-
6
[:accredited_body_code]
-
end
-
-
2
def redirect_to_provider_search
-
redirect_to(
-
accredited_body_search_provider_recruitment_cycle_course_path(
-
@course.provider_code,
-
@course.recruitment_cycle_year,
-
@course.course_code,
-
query: update_course_params[:accredited_body],
-
),
-
)
-
end
-
-
2
def redirect_to_update_successful
-
flash[:success] = I18n.t("success.saved")
-
redirect_to(
-
details_provider_recruitment_cycle_course_path(
-
@course.provider_code,
-
@course.recruitment_cycle_year,
-
@course.course_code,
-
),
-
)
-
end
-
-
2
def current_step
-
6
:accredited_body
-
end
-
-
2
def build_course_params
-
2
@accredited_body = params[:course].delete(:accredited_body)
-
2
@autocompleted_provider_code = params[:course].delete(:autocompleted_provider_code)
-
end
-
-
2
def errors_for_search_query(code, query)
-
2
errors = {}
-
-
2
if other_selected_with_no_autocompleted_code?(code) && query.length < 2
-
errors = { accredited_body: ["Accredited body search too short, enter 2 or more characters"] }
-
2
elsif code.blank?
-
1
errors = { accredited_body_code: ["Pick an accredited body"] }
-
end
-
-
2
errors
-
end
-
-
2
def build_course
-
@course = Course
-
.where(recruitment_cycle_year: params[:recruitment_cycle_year])
-
.where(provider_code: params[:provider_code])
-
.includes(:accrediting_provider)
-
.find(params[:code])
-
.first
-
end
-
-
2
def update_course_params
-
params.require(:course).permit(
-
:autocompleted_provider_code,
-
:accredited_body_code,
-
:accredited_body,
-
)
-
end
-
-
2
def update_params
-
autocompleted_code = update_course_params[:autocompleted_provider_code]
-
code = update_course_params[:accredited_body_code]
-
-
{
-
accredited_body_code: autocompleted_code.presence || code,
-
}
-
end
-
-
2
def other_selected_with_no_autocompleted_code?(code)
-
3
code == "other" && @autocompleted_provider_code.blank?
-
end
-
end
-
end
-
end
-
3
module Publish
-
3
module Courses
-
3
class AgeRangeController < PublishController
-
3
include CourseBasicDetailConcern
-
3
decorates_assigned :course
-
-
3
def edit
-
3
if params[:display_errors] == "true"
-
form_object.valid?
-
end
-
-
3
render locals: { form_object: form_object }
-
end
-
-
3
def update
-
3
if form_object.valid?
-
2
course_details_success_message("age range")
-
-
2
update_age_range_param
-
-
2
if @course.update(course_params)
-
2
redirect_to(
-
details_publish_provider_recruitment_cycle_course_path(
-
@course.provider_code,
-
@course.recruitment_cycle_year,
-
@course.course_code,
-
),
-
)
-
end
-
else
-
1
render :edit, locals: { form_object: form_object }
-
end
-
end
-
-
3
private
-
-
3
def form_object
-
7
@form_object ||= AgeRangeForm.new(@course, params: permitted_params)
-
end
-
-
3
def permitted_params
-
6
if params[:course]
-
3
(params[:course] || ActionController::Parameters.new).permit(:age_range_in_years, :course_age_range_in_years_other_from, :course_age_range_in_years_other_to)
-
else
-
99
@course.attributes.select { |k, _v| %w[age_range_in_years course_age_range_in_years_other_from course_age_range_in_years_other_to].include?(k) }
-
end
-
end
-
-
3
def error_keys
-
29
[:age_range_in_years]
-
end
-
-
3
def update_age_range_param
-
2
params[:course][:age_range_in_years] = "#{age_from_param}_to_#{age_to_param}" if valid_custom_age_range?
-
end
-
-
3
def valid_custom_age_range?
-
2
age_from_param.present? && age_to_param.present? && age_range_is_other?
-
end
-
-
3
def age_to_param
-
3
course_param[:course_age_range_in_years_other_to]
-
end
-
-
3
def age_from_param
-
4
course_param[:course_age_range_in_years_other_from]
-
end
-
-
3
def age_range_param
-
1
course_param[:age_range_in_years]
-
end
-
-
3
def age_range_is_other?
-
1
age_range_param == "other"
-
end
-
-
3
def course_param
-
8
params[:course]
-
end
-
-
3
def current_step
-
15
:age_range
-
end
-
-
3
def build_course
-
6
super
-
6
authorize @course
-
end
-
end
-
end
-
end
-
2
module Publish
-
2
module Courses
-
2
class ApplicationsOpenController < PublishController
-
2
before_action :build_recruitment_cycle
-
2
before_action :build_course_params, only: %i[update continue]
-
2
include CourseBasicDetailConcern
-
-
2
def update
-
super
-
end
-
-
2
def continue
-
4
super
-
end
-
-
2
private
-
-
2
def actual_params
-
10
params.require(:course)
-
.except(
-
:qualification,
-
:maths,
-
:english,
-
:science,
-
:funding_type,
-
:level,
-
:is_send,
-
:study_mode,
-
:age_range_in_years,
-
:sites_ids,
-
:subjects_ids,
-
:goto_confirmation,
-
:accredited_body_code,
-
)
-
.permit(
-
:start_date,
-
:applications_open_from,
-
:day,
-
:month,
-
:year,
-
)
-
end
-
-
# This is needed to handle the fact that dates are optionally specified as year/month/day in the UI
-
# This method assigns the params to the correct YYYY-MM-DD value given what is selected
-
2
def build_course_params
-
4
if params.key?(:course)
-
applications_open_from =
-
4
if actual_params["applications_open_from"] == "other"
-
1
"#{actual_params['year']}-#{actual_params['month']}-#{actual_params['day']}"
-
else
-
3
actual_params["applications_open_from"]
-
end
-
4
params["course"]["applications_open_from"] = applications_open_from
-
else
-
ActionController::Parameters.new({}).permit
-
end
-
end
-
-
2
def build_recruitment_cycle
-
9
cycle_year = params.fetch(
-
:recruitment_cycle_year,
-
Settings.current_recruitment_cycle_year,
-
)
-
-
9
@recruitment_cycle = RecruitmentCycle.find_by(year: cycle_year)
-
end
-
-
2
def current_step
-
12
:applications_open
-
end
-
-
2
def error_keys
-
17
[:applications_open_from]
-
end
-
end
-
end
-
end
-
2
module Publish
-
2
module Courses
-
2
class ApprenticeshipController < PublishController
-
2
include CourseBasicDetailConcern
-
-
2
private
-
-
2
def current_step
-
11
:apprenticeship
-
end
-
-
2
def error_keys
-
26
%i[funding_type program_type]
-
end
-
end
-
end
-
end
-
module Publish
-
module Courses
-
class BaseFundingTypeController < PublishController
-
include SuccessMessage
-
-
private
-
-
def course
-
@course ||= CourseDecorator.new(provider.courses.find_by!(course_code: params[:code]))
-
end
-
-
def course_enrichment
-
@course_enrichment ||= course.enrichments.find_or_initialize_draft
-
end
-
-
def funding_type_params
-
params.require(funding_type).permit(*funding_type_fields)
-
end
-
-
def formatted_params
-
if funding_type_params[:course_length] == "Other" && funding_type_params[:course_length_other_length].present?
-
funding_type_params.merge(
-
course_length: funding_type_params[:course_length_other_length],
-
)
-
else
-
funding_type_params
-
end
-
end
-
-
def funding_type
-
raise NotImplementedError
-
end
-
-
def funding_type_fields
-
raise NotImplementedError
-
end
-
end
-
end
-
end
-
3
module Publish
-
3
module Courses
-
3
class CourseInformationController < PublishController
-
3
include CopyCourseContent
-
-
3
def edit
-
14
authorize(provider)
-
-
14
@course_information_form = CourseInformationForm.new(course_enrichment)
-
14
copy_content_check(::Courses::Copy::ABOUT_FIELDS)
-
-
14
@course_information_form.valid? if show_errors_on_publish?
-
end
-
-
3
def update
-
3
authorize(provider)
-
-
3
@course_information_form = CourseInformationForm.new(course_enrichment, params: course_information_params)
-
-
3
if @course_information_form.save!
-
2
course_description_success_message("course information")
-
-
2
redirect_to publish_provider_recruitment_cycle_course_path(
-
provider.provider_code,
-
recruitment_cycle.year,
-
course.course_code,
-
)
-
else
-
1
render :edit
-
end
-
end
-
-
3
private
-
-
3
def course
-
45
@course ||= CourseDecorator.new(provider.courses.find_by!(course_code: params[:code]))
-
end
-
-
3
def course_information_params
-
3
params
-
.require(:publish_course_information_form)
-
.permit(
-
CourseInformationForm::FIELDS,
-
)
-
end
-
-
3
def course_enrichment
-
17
@course_enrichment ||= course.enrichments.find_or_initialize_draft
-
end
-
end
-
end
-
end
-
1
module Publish
-
1
module Courses
-
1
module Degrees
-
1
class GradeController < PublishController
-
1
def edit
-
4
authorize(provider)
-
-
4
@grade_form = DegreeGradeForm.build_from_course(course)
-
end
-
-
1
def update
-
3
authorize(provider)
-
-
3
@grade_form = DegreeGradeForm.new(grade: grade_params)
-
-
3
if course.is_primary? && @grade_form.save(course)
-
1
course_description_success_message("minimum degree classification")
-
-
1
redirect_to publish_provider_recruitment_cycle_course_path
-
2
elsif @grade_form.save(course)
-
1
redirect_to degrees_subject_requirements_publish_provider_recruitment_cycle_course_path
-
else
-
1
@errors = @grade_form.errors.messages
-
1
render :edit
-
end
-
end
-
-
1
private
-
-
1
def course
-
10
@course ||= CourseDecorator.new(provider.courses.find_by!(course_code: params[:code]))
-
end
-
-
1
def grade_params
-
3
return if params[:publish_degree_grade_form].blank?
-
-
2
params.require(:publish_degree_grade_form).permit(:grade)[:grade]
-
end
-
end
-
end
-
end
-
end
-
1
module Publish
-
1
module Courses
-
1
module Degrees
-
1
class StartController < PublishController
-
1
def edit
-
6
authorize(provider)
-
-
6
@start_form = DegreeStartForm.new
-
6
@start_form.build_from_course(course)
-
6
@start_form.valid? if show_errors_on_publish?
-
end
-
-
1
def update
-
5
authorize(provider)
-
-
5
@start_form = DegreeStartForm.new(degree_grade_required: grade_required_params)
-
-
5
if course.is_primary? && @start_form.save(course)
-
1
flash[:success] = I18n.t("success.value_saved", value: "minimum degree classification")
-
-
1
redirect_to publish_provider_recruitment_cycle_course_path
-
4
elsif @start_form.save(course)
-
1
redirect_to degrees_subject_requirements_publish_provider_recruitment_cycle_course_path
-
3
elsif @start_form.degree_grade_required.present?
-
2
redirect_to degrees_grade_publish_provider_recruitment_cycle_course_path
-
else
-
1
@errors = @start_form.errors.messages
-
1
render :edit
-
end
-
end
-
-
1
private
-
-
1
def course
-
17
@course ||= CourseDecorator.new(provider.courses.find_by!(course_code: params[:code]))
-
end
-
-
1
def grade_required_params
-
5
return if params[:publish_degree_start_form].blank?
-
-
4
params.require(:publish_degree_start_form).permit(:degree_grade_required)[:degree_grade_required]
-
end
-
end
-
end
-
end
-
end
-
1
module Publish
-
1
module Courses
-
1
module Degrees
-
1
class SubjectRequirementsController < PublishController
-
1
include CopyCourseContent
-
1
decorates_assigned :source_course
-
1
before_action :redirect_to_course_details_page_if_course_is_primary
-
-
1
def edit
-
8
authorize(provider)
-
-
8
set_backlink
-
8
@subject_requirements_form = SubjectRequirementForm.build_from_course(course)
-
8
copy_boolean_check(::Courses::Copy::SUBJECT_REQUIREMENTS_FIELDS)
-
end
-
-
1
def update
-
3
authorize(provider)
-
-
3
@subject_requirements_form = SubjectRequirementForm.new(subject_requirements_params)
-
-
3
if @subject_requirements_form.save(@course)
-
2
course_description_success_message("degree requirements")
-
-
2
redirect_to publish_provider_recruitment_cycle_course_path
-
else
-
1
set_backlink
-
1
@errors = @subject_requirements_form.errors.messages
-
1
render :edit
-
end
-
end
-
-
1
private
-
-
1
def course
-
44
@course ||= CourseDecorator.new(provider.courses.find_by!(course_code: params[:code]))
-
end
-
-
1
def subject_requirements_params
-
3
params
-
.require(:publish_subject_requirement_form)
-
.permit(:additional_degree_subject_requirements, :degree_subject_requirements)
-
end
-
-
1
def set_backlink
-
9
@backlink = if course.degree_grade == "not_required"
-
1
degrees_start_publish_provider_recruitment_cycle_course_path
-
else
-
8
degrees_grade_publish_provider_recruitment_cycle_course_path
-
end
-
end
-
-
1
def redirect_to_course_details_page_if_course_is_primary
-
11
redirect_to publish_provider_recruitment_cycle_course_path if course.is_primary?
-
end
-
end
-
end
-
end
-
end
-
1
module Publish
-
1
module Courses
-
1
class DeletionsController < PublishController
-
6
before_action :redirect_to_courses, if: -> { course.is_published? }
-
-
1
def edit
-
2
authorize(provider)
-
-
2
@course_deletion_form = CourseDeletionForm.new(course)
-
end
-
-
1
def destroy
-
2
authorize(provider)
-
-
2
@course_deletion_form = CourseDeletionForm.new(course, params: deletion_params)
-
-
2
if @course_deletion_form.destroy!
-
1
flash[:success] = "#{@course.name} (#{@course.course_code}) has been deleted"
-
-
1
redirect_to publish_provider_recruitment_cycle_courses_path(
-
provider.provider_code,
-
recruitment_cycle.year,
-
)
-
else
-
1
render :edit
-
end
-
end
-
-
1
private
-
-
1
def redirect_to_courses
-
1
redirect_to publish_provider_recruitment_cycle_courses_path(
-
provider.provider_code,
-
course.recruitment_cycle_year,
-
)
-
end
-
-
1
def course
-
10
@course ||= CourseDecorator.new(provider.courses.find_by!(course_code: params[:code]))
-
end
-
-
1
def deletion_params
-
2
params.require(:publish_course_deletion_form).permit(CourseDeletionForm::FIELDS)
-
end
-
end
-
end
-
end
-
1
module Publish
-
1
module Courses
-
1
class FeeOrSalaryController < PublishController
-
1
include CourseBasicDetailConcern
-
-
1
private
-
-
1
def current_step
-
14
:fee_or_salary
-
end
-
-
1
def error_keys
-
29
%i[funding_type program_type]
-
end
-
end
-
end
-
end
-
module Publish
-
module Courses
-
class FeesController < BaseFundingTypeController
-
include CopyCourseContent
-
decorates_assigned :source_course
-
-
def edit
-
authorize(provider)
-
-
@course_fee_form = CourseFeeForm.new(course_enrichment)
-
copy_content_check(::Courses::Copy::FEES_FIELDS)
-
@course_fee_form.valid? if show_errors_on_publish?
-
end
-
-
def update
-
authorize(provider)
-
-
@course_fee_form = CourseFeeForm.new(course_enrichment, params: formatted_params)
-
-
if @course_fee_form.save!
-
course_description_success_message("course length and fees")
-
-
redirect_to publish_provider_recruitment_cycle_course_path(
-
provider.provider_code,
-
recruitment_cycle.year,
-
course.course_code,
-
)
-
else
-
render :edit
-
end
-
end
-
-
private
-
-
def funding_type
-
:publish_course_fee_form
-
end
-
-
def funding_type_fields
-
CourseFeeForm::FIELDS
-
end
-
end
-
end
-
end
-
2
module Publish
-
2
module Courses
-
2
class GcseRequirementsController < PublishController
-
2
include CopyCourseContent
-
2
decorates_assigned :source_course
-
-
2
def edit
-
7
authorize(provider)
-
-
7
@gcse_requirements_form = GcseRequirementsForm.build_from_course(course)
-
7
copy_boolean_check(::Courses::Copy::GCSE_FIELDS)
-
7
@gcse_requirements_form.valid? if show_errors_on_publish?
-
end
-
-
2
def update
-
3
authorize(provider)
-
-
3
@gcse_requirements_form = GcseRequirementsForm.new(**gcse_requirements_form_params.merge(level: course.level))
-
-
3
if @gcse_requirements_form.save(course)
-
1
course_description_success_message("GCSE requirements")
-
-
1
redirect_to publish_provider_recruitment_cycle_course_path
-
else
-
2
@errors = @gcse_requirements_form.errors.messages
-
2
render :edit
-
end
-
end
-
-
2
private
-
-
2
def course
-
32
@course ||= CourseDecorator.new(provider.courses.find_by!(course_code: params[:code]))
-
end
-
-
2
def translatable_params
-
3
%i[accept_pending_gcse
-
accept_gcse_equivalency
-
accept_english_gcse_equivalency
-
accept_maths_gcse_equivalency
-
accept_science_gcse_equivalency]
-
end
-
-
2
def gcse_requirements_form_params
-
3
translatable_params.index_with do |key|
-
15
translate_params(publish_gcse_requirements_form_params[key])
-
end.merge(additional_gcse_equivalencies: helpers.raw(publish_gcse_requirements_form_params[:additional_gcse_equivalencies]))
-
end
-
-
2
def publish_gcse_requirements_form_params
-
18
@publish_gcse_requirements_form_params ||= params.require(:publish_gcse_requirements_form).permit(
-
:accept_pending_gcse,
-
:accept_english_gcse_equivalency,
-
:accept_gcse_equivalency,
-
{ accept_english_gcse_equivalency: [] },
-
{ accept_maths_gcse_equivalency: [] },
-
{ accept_science_gcse_equivalency: [] },
-
:additional_gcse_equivalencies,
-
)
-
end
-
-
2
def translate_params(value)
-
15
return if value.blank?
-
-
6
if value.is_a?(Array)
-
2
(%w[Maths English Science] & value).present?
-
else
-
4
value == "true"
-
end
-
end
-
end
-
end
-
end
-
3
module Publish
-
3
module Courses
-
3
class LevelController < PublishController
-
3
include CourseBasicDetailConcern
-
-
3
private
-
-
3
def error_keys
-
56
[:level]
-
end
-
-
3
def current_step
-
18
:level
-
end
-
end
-
end
-
end
-
4
module Publish
-
4
module Courses
-
4
class ModernLanguagesController < PublishController
-
4
decorates_assigned :course
-
4
before_action :build_course, only: %i[edit update]
-
4
before_action :build_course_params, only: [:continue]
-
4
include CourseBasicDetailConcern
-
-
4
def new
-
7
authorize(@provider, :can_create_course?)
-
7
return if has_modern_languages_subject?
-
-
4
redirect_to next_step
-
end
-
-
4
def continue
-
2
super
-
end
-
-
4
def edit
-
4
authorize(provider)
-
-
4
return if selected_non_language_subjects_ids.include? modern_languages_subject_id.to_s
-
-
1
redirect_to(
-
details_publish_provider_recruitment_cycle_course_path(
-
@course.provider_code,
-
@course.recruitment_cycle_year,
-
@course.course_code,
-
),
-
)
-
end
-
-
4
def update
-
2
authorize(provider)
-
-
2
if course_subjects_form.save!
-
1
flash[:success] = I18n.t("success.saved")
-
1
redirect_to(
-
details_publish_provider_recruitment_cycle_course_path(
-
@course.provider_code,
-
@course.recruitment_cycle_year,
-
@course.course_code,
-
),
-
)
-
else
-
1
@errors = @course.errors.messages
-
1
render :edit
-
end
-
end
-
-
4
def back
-
authorize(@provider, :edit?)
-
if has_modern_languages_subject?
-
redirect_to new_publish_provider_recruitment_cycle_courses_modern_languages_path(path_params)
-
else
-
redirect_to @back_link_path
-
end
-
end
-
-
4
def current_step
-
14
:modern_languages
-
end
-
-
4
private
-
-
4
def updated_subject_list
-
2
@updated_subject_list ||= selected_language_subjects_ids.concat(selected_non_language_subjects_ids)
-
end
-
-
4
def course_subjects_form
-
2
@course_subjects_form ||= CourseSubjectsForm.new(@course, params: updated_subject_list)
-
end
-
-
4
def error_keys
-
17
[:modern_languages_subjects]
-
end
-
-
4
def modern_languages_subject_id
-
10
@modern_languages_subject_id ||= @course.edit_course_options[:modern_languages_subject].id
-
end
-
-
4
def selected_subjects(param_key)
-
8
edit_course_options_key = param_key == :language_ids ? :modern_languages : :subjects
-
-
8
ids = params.dig(:course, param_key)&.map(&:to_i) || []
-
-
8
@course.edit_course_options[edit_course_options_key].filter_map do |subject|
-
192
subject.id.to_s if ids.include?(subject.id)
-
end
-
end
-
-
4
def selected_language_subjects_ids
-
2
selected_subjects(:language_ids)
-
end
-
-
4
def selected_non_language_subjects_ids
-
6
selected_subjects(:subjects_ids)
-
end
-
-
4
def has_modern_languages_subject?
-
13
@course.subjects.any? { |subject| subject.id == modern_languages_subject_id }
-
end
-
-
4
def build_course_params
-
2
build_new_course # to get languages edit_options
-
2
params[:course][:subjects_ids] = selected_non_language_subject_ids
-
2
params[:course][:subjects_ids] += params[:course][:language_ids] if params[:course][:language_ids]
-
2
params[:course].delete(:language_ids)
-
end
-
-
4
def non_language_subject_ids
-
2
@course.edit_course_options[:subjects].map(&:id).map(&:to_s)
-
end
-
-
4
def selected_non_language_subject_ids
-
2
non_language_subject_ids & params[:course][:subjects_ids]
-
end
-
end
-
end
-
end
-
3
module Publish
-
3
module Courses
-
3
class OutcomeController < PublishController
-
3
include CourseBasicDetailConcern
-
3
before_action :order_edit_options, only: %i[edit new]
-
-
3
def edit
-
3
super
-
end
-
-
3
def update
-
1
authorize(provider)
-
-
1
@errors = errors
-
1
return render :edit if @errors.present?
-
-
1
if @course.update(course_params)
-
1
course_details_success_message("course outcome")
-
-
1
redirect_to(
-
details_publish_provider_recruitment_cycle_course_path(
-
@course.provider_code,
-
@course.recruitment_cycle_year,
-
@course.course_code,
-
),
-
)
-
else
-
@errors = @course.errors.messages
-
render :edit
-
end
-
end
-
-
3
def new
-
8
super
-
end
-
-
3
private
-
-
3
def order_edit_options
-
11
qualification_options = @course.edit_course_options[:qualifications]
-
11
@course.edit_course_options[:qualifications] = if @course.level == "further_education"
-
2
non_qts_qualifications(qualification_options)
-
else
-
9
qts_qualifications(qualification_options)
-
end
-
end
-
-
3
def current_step
-
17
:outcome
-
end
-
-
3
def qts_qualifications(edit_options)
-
9
options = %w[pgce_with_qts qts pgde_with_qts]
-
-
9
if edit_options.sort != options.sort
-
raise "Non QTS qualification options do not match"
-
end
-
-
9
options
-
end
-
-
3
def non_qts_qualifications(edit_options)
-
2
options = %w[pgce pgde]
-
-
2
if edit_options.sort != options.sort
-
raise "QTS qualification options do not match"
-
end
-
-
2
options
-
end
-
-
3
def errors
-
6
params.dig(:course, :qualification) ? {} : { qualification: ["Pick an outcome"] }
-
end
-
end
-
end
-
end
-
1
module Publish
-
1
module Courses
-
1
class RequirementsController < PublishController
-
1
include CopyCourseContent
-
1
decorates_assigned :source_course
-
-
1
def edit
-
8
authorize(provider)
-
-
8
@course_requirement_form = CourseRequirementForm.new(course_enrichment)
-
8
if @course.recruitment_cycle_year.to_i >= Provider::CHANGES_INTRODUCED_IN_2022_CYCLE
-
8
copy_content_check(::Courses::Copy::POST_2022_CYCLE_REQUIREMENTS_FIELDS)
-
else
-
copy_content_check(::Courses::Copy::PRE_2022_CYCLE_REQUIREMENTS_FIELDS)
-
end
-
end
-
-
1
def update
-
2
authorize(provider)
-
-
2
@course_requirement_form = CourseRequirementForm.new(course_enrichment, params: course_requirement_params)
-
-
2
if @course_requirement_form.save!
-
1
flash[:success] = I18n.t("success.saved")
-
-
1
redirect_to publish_provider_recruitment_cycle_course_path(
-
provider.provider_code,
-
recruitment_cycle.year,
-
course.course_code,
-
)
-
else
-
1
render :edit
-
end
-
end
-
-
1
private
-
-
1
def course
-
27
@course ||= CourseDecorator.new(provider.courses.find_by!(course_code: params[:code]))
-
end
-
-
1
def course_requirement_params
-
2
params
-
.require(:publish_course_requirement_form)
-
.permit(
-
CourseRequirementForm::FIELDS,
-
)
-
end
-
-
1
def course_enrichment
-
10
@course_enrichment ||= course.enrichments.find_or_initialize_draft
-
end
-
end
-
end
-
end
-
module Publish
-
module Courses
-
class SalaryController < BaseFundingTypeController
-
def edit
-
authorize(provider)
-
-
@course_salary_form = CourseSalaryForm.new(course_enrichment)
-
@course_salary_form.valid? if show_errors_on_publish?
-
end
-
-
def update
-
authorize(provider)
-
-
@course_salary_form = CourseSalaryForm.new(course_enrichment, params: formatted_params)
-
-
if @course_salary_form.save!
-
flash[:success] = I18n.t("success.saved")
-
-
redirect_to publish_provider_recruitment_cycle_course_path(
-
provider.provider_code,
-
recruitment_cycle.year,
-
course.course_code,
-
)
-
else
-
render :edit
-
end
-
end
-
-
private
-
-
def funding_type
-
:publish_course_salary_form
-
end
-
-
def funding_type_fields
-
CourseSalaryForm::FIELDS
-
end
-
end
-
end
-
end
-
3
module Publish
-
3
module Courses
-
3
class SitesController < PublishController
-
3
before_action :build_course_params, only: %i[continue]
-
-
3
include CourseBasicDetailConcern
-
-
3
def continue
-
3
super
-
end
-
-
3
def new
-
6
authorize(@provider, :edit?)
-
6
if @provider.sites.count == 1
-
set_default_site
-
redirect_to next_step
-
end
-
end
-
-
3
def edit
-
2
authorize(provider)
-
-
2
@course_location_form = CourseLocationForm.new(@course)
-
end
-
-
3
def update
-
2
authorize(provider)
-
-
2
@course_location_form = CourseLocationForm.new(@course, params: location_params)
-
2
if @course_location_form.save!
-
1
course_details_success_message("course locations")
-
-
1
redirect_to details_publish_provider_recruitment_cycle_course_path(
-
provider.provider_code,
-
recruitment_cycle.year,
-
course.course_code,
-
)
-
else
-
1
render :edit
-
end
-
end
-
-
3
def back
-
authorize(@provider, :edit?)
-
if @provider.sites.count > 1
-
redirect_to new_publish_provider_recruitment_cycle_courses_locations_path(path_params)
-
else
-
redirect_to @back_link_path
-
end
-
end
-
-
3
private
-
-
3
def current_step
-
11
:location
-
end
-
-
3
def error_keys
-
11
[:sites]
-
end
-
-
3
def set_default_site
-
params["course"] ||= {}
-
params["course"]["sites_ids"] = [@provider.sites.first.id]
-
end
-
-
3
def build_course_params
-
3
selected_site_ids = params.dig(:course, :site_statuses_attributes)
-
.values
-
8
.select { |field| field["selected"] == "1" }
-
4
.map { |field| field["id"] }
-
-
3
params["course"]["sites_ids"] = selected_site_ids
-
3
params["course"].delete("site_statuses_attributes")
-
end
-
-
3
def location_params
-
2
return { site_ids: nil } if params[:publish_course_location_form][:site_ids].all?(&:empty?)
-
-
1
params.require(:publish_course_location_form).permit(site_ids: [])
-
end
-
-
3
def build_course
-
4
@course = provider.courses.find_by!(course_code: params[:code])
-
end
-
end
-
end
-
end
-
2
module Publish
-
2
module Courses
-
2
class StartDateController < PublishController
-
2
include CourseBasicDetailConcern
-
-
2
private
-
-
2
def current_step
-
8
:start_date
-
end
-
-
2
def error_keys
-
3
[:start_date]
-
end
-
end
-
end
-
end
-
4
module Publish
-
4
module Courses
-
4
class StudyModeController < PublishController
-
4
include CourseBasicDetailConcern
-
-
4
def edit
-
2
authorize(provider)
-
-
2
@course_study_mode_form = CourseStudyModeForm.new(@course)
-
end
-
-
4
def update
-
2
authorize(provider)
-
-
2
@course_study_mode_form = CourseStudyModeForm.new(@course, params: study_mode_params)
-
2
if @course_study_mode_form.save!
-
1
course_description_success_message("full or part time")
-
-
1
redirect_to details_publish_provider_recruitment_cycle_course_path(
-
provider.provider_code,
-
recruitment_cycle.year,
-
course.course_code,
-
)
-
else
-
1
render :edit
-
end
-
end
-
-
4
private
-
-
4
def study_mode_params
-
2
return { study_mode: nil } if params[:publish_course_study_mode_form].blank?
-
-
1
params.require(:publish_course_study_mode_form).permit(*CourseStudyModeForm::FIELDS)
-
end
-
-
4
def current_step
-
19
:full_or_part_time
-
end
-
-
4
def errors
-
5
params.dig(:course, :study_mode) ? {} : { study_mode: ["Pick full time, part time or full time and part time"] }
-
end
-
end
-
end
-
end
-
3
module Publish
-
3
module Courses
-
3
class SubjectsController < PublishController
-
3
decorates_assigned :course
-
3
before_action :build_course, only: %i[edit update]
-
3
before_action :build_course_params, only: [:continue]
-
3
include CourseBasicDetailConcern
-
-
3
def edit
-
3
authorize(provider)
-
end
-
-
3
def continue
-
5
super
-
end
-
-
3
def update
-
3
authorize(provider)
-
-
3
if selected_subject_ids.include?(modern_languages_subject_id.to_s)
-
1
redirect_to(
-
modern_languages_publish_provider_recruitment_cycle_course_path(
-
@course.provider_code,
-
@course.recruitment_cycle_year,
-
@course.course_code,
-
course: { subjects_ids: selected_subject_ids },
-
),
-
)
-
-
2
elsif course_subjects_form.save!
-
2
value = @course.is_primary? ? "primary subject" : "secondary subject"
-
2
course_details_success_message(value)
-
-
2
redirect_to(
-
details_publish_provider_recruitment_cycle_course_path(
-
@course.provider_code,
-
@course.recruitment_cycle_year,
-
@course.course_code,
-
),
-
)
-
else
-
@errors = @course.errors.messages
-
render :edit
-
end
-
end
-
-
3
private
-
-
3
def course_subjects_form
-
2
@course_subjects_form ||= CourseSubjectsForm.new(@course, params: selected_subject_ids)
-
end
-
-
3
def modern_languages_subject_id
-
8
@modern_languages_subject_id ||= @course.edit_course_options[:modern_languages_subject].id
-
end
-
-
3
def selected_subject_ids
-
21
@selected_subject_ids ||= [selected_master, selected_subordinate].compact
-
end
-
-
3
def current_step
-
17
:subjects
-
end
-
-
3
def error_keys
-
42
[:subjects]
-
end
-
-
3
def selected_master
-
8
@selected_master ||= params[:course][:master_subject_id] if params[:course][:master_subject_id].present?
-
end
-
-
3
def selected_subordinate
-
8
@selected_subordinate ||= params[:course][:subordinate_subject_id] if params[:course][:subordinate_subject_id].present?
-
end
-
-
3
def build_course_params
-
5
previous_subject_selections = params[:course][:subjects_ids]
-
-
5
params[:course][:subjects_ids] = selected_subject_ids
-
-
5
params[:course].delete(:master_subject_id)
-
5
params[:course].delete(:subordinate_subject_id)
-
-
5
build_new_course # to get languages edit_options
-
-
5
previous_language_selections = selected_subject_ids.include?(modern_languages_subject_id.to_s) ? strip_non_language_subject_ids(previous_subject_selections) : []
-
-
5
params[:course][:subjects_ids] = selected_subject_ids.concat(previous_language_selections)
-
end
-
-
3
def strip_non_language_subject_ids(subject_ids)
-
1
return [] unless subject_ids
-
-
subject_ids.filter { |id| available_languages_ids.include?(id) }
-
end
-
-
3
def available_languages_ids
-
@course.edit_course_options[:modern_languages].map(&:id).map(&:to_s)
-
end
-
end
-
end
-
end
-
1
module Publish
-
1
module Courses
-
1
class VacanciesController < PublishController
-
1
def edit
-
15
authorize(provider)
-
-
15
@course_vacancies_form = CourseVacanciesForm.new(course)
-
15
@site_statuses = @course_vacancies_form.running_site_statuses
-
end
-
-
1
def update
-
11
authorize(provider)
-
-
11
@course_vacancies_form = CourseVacanciesForm.new(course, params: vacancy_params)
-
-
11
if @course_vacancies_form.save!
-
9
flash[:success] = I18n.t("success.published")
-
-
9
redirect_to publish_provider_recruitment_cycle_courses_path(
-
provider.provider_code,
-
recruitment_cycle.year,
-
)
-
else
-
2
@site_statuses = @course_vacancies_form.running_site_statuses
-
-
2
render :edit
-
end
-
end
-
-
1
private
-
-
1
def course
-
26
@course ||= provider.courses.find_by!(course_code: params[:code])
-
end
-
-
1
def vacancy_params
-
11
return { change_vacancies_confirmation: nil } if params[:publish_course_vacancies_form].blank?
-
-
9
params
-
.require(:publish_course_vacancies_form)
-
.permit(
-
CourseVacanciesForm::FIELDS,
-
site_statuses_attributes: %i[id vac_status full_time part_time],
-
)
-
end
-
end
-
end
-
end
-
1
module Publish
-
1
module Courses
-
1
class WithdrawalsController < PublishController
-
1
before_action :redirect_to_courses, if: :course_withdrawn?
-
7
before_action :redirect_to_courses, unless: -> { course.is_published? }
-
-
1
def edit
-
2
authorize(provider)
-
-
2
@course_withdrawal_form = CourseWithdrawalForm.new(course)
-
end
-
-
1
def update
-
2
authorize(provider)
-
-
2
@course_withdrawal_form = CourseWithdrawalForm.new(course, params: withdrawal_params)
-
-
2
if @course_withdrawal_form.save!
-
1
flash[:success] = "#{course.name} (#{course.course_code}) has been withdrawn"
-
-
1
redirect_to publish_provider_recruitment_cycle_courses_path(
-
provider.provider_code,
-
recruitment_cycle.year,
-
)
-
else
-
1
render :edit
-
end
-
end
-
-
1
private
-
-
1
def redirect_to_courses
-
2
message = if course_withdrawn?
-
1
"#{course.name} (#{course.course_code}) has already been withdrawn"
-
else
-
1
"Courses that have not been published should be deleted not withdrawn"
-
end
-
-
2
flash[:error] = { id: "withdraw-error", message: message }
-
-
2
redirect_to publish_provider_recruitment_cycle_courses_path(
-
provider.provider_code,
-
course.recruitment_cycle_year,
-
)
-
end
-
-
1
def course
-
18
@course ||= CourseDecorator.new(provider.courses.find_by!(course_code: params[:code]))
-
end
-
-
1
def course_withdrawn?
-
2
course.content_status == :withdrawn
-
end
-
-
1
def withdrawal_params
-
2
return { course_code: nil } if params[:publish_course_withdrawal_form].blank?
-
-
2
params
-
.require(:publish_course_withdrawal_form)
-
.permit(CourseWithdrawalForm::FIELDS)
-
end
-
end
-
end
-
end
-
4
module Publish
-
4
class CoursesController < PublishController
-
4
decorates_assigned :course
-
-
4
def index
-
27
authorize :provider, :index?
-
-
27
courses_by_accrediting_provider
-
27
self_accredited_courses
-
end
-
-
4
def show
-
23
fetch_course
-
-
23
authorize @course
-
-
23
@errors = flash[:error_summary]
-
23
flash.delete(:error_summary)
-
end
-
-
4
def details
-
11
fetch_course
-
-
11
authorize @course
-
end
-
-
4
def new
-
2
authorize(provider, :can_create_course?)
-
2
return render_locations_messages unless provider.sites&.any?
-
-
2
redirect_to new_publish_provider_recruitment_cycle_courses_level_path(params[:provider_code], @recruitment_cycle.year)
-
end
-
-
4
def create
-
2
authorize(provider, :can_create_course?)
-
2
@course = ::Courses::CreationService.call(course_params: course_params, provider: provider, next_available_course_code: true)
-
-
2
if @course.save
-
2
flash[:success_with_body] = { title: "Your course has been created", body: "Add the rest of your details and publish the course, so that candidates can find and apply to it." }
-
2
redirect_to(
-
publish_provider_recruitment_cycle_courses_path(
-
@course.provider_code,
-
@course.recruitment_cycle.year,
-
),
-
)
-
else
-
@errors = @course.errors.messages
-
@course_creation_params = course_params
-
-
render :confirmation
-
end
-
end
-
-
4
def confirmation
-
4
authorize(provider, :can_create_course?)
-
-
4
@course_creation_params = course_params
-
4
@course = ::Courses::CreationService.call(course_params: course_params, provider: provider)
-
end
-
-
4
def preview
-
2
fetch_course
-
-
2
authorize @course
-
end
-
-
4
def publish
-
4
fetch_course
-
4
authorize @course
-
-
4
if @course.publishable?
-
3
publish_course
-
3
flash[:success] = "Your course has been published."
-
-
3
redirect_to publish_provider_recruitment_cycle_course_path(
-
@provider.provider_code,
-
@course.recruitment_cycle_year,
-
@course.course_code,
-
)
-
else
-
1
@errors = format_publish_error_messages
-
-
1
fetch_course
-
1
render :show
-
end
-
end
-
-
4
private
-
-
4
def course_params
-
10
if params.key? :course
-
10
params.require(:course)
-
.permit(
-
policy(Course.new).permitted_new_course_attributes,
-
sites_ids: [],
-
subjects_ids: [],
-
)
-
else
-
ActionController::Parameters.new({}).permit(:course)
-
end
-
end
-
-
4
def render_locations_messages
-
flash[:error] = { id: "locations-error", message: "You need to create at least one location before creating a course" }
-
-
redirect_to new_publish_provider_recruitment_cycle_location_path(provider.provider_code, provider.recruitment_cycle_year)
-
end
-
-
4
def fetch_course
-
41
@course = provider.courses.find_by!(course_code: params[:code])
-
end
-
-
4
def provider
-
111
@provider ||= recruitment_cycle.providers
-
.includes(courses: %i[sites site_statuses enrichments provider])
-
.find_by!(provider_code: params[:provider_code])
-
end
-
-
4
def courses_by_accrediting_provider
-
54
@courses_by_accrediting_provider ||= ::Courses::Fetch.by_accrediting_provider(provider)
-
end
-
-
4
def self_accredited_courses
-
27
@self_accredited_courses ||= courses_by_accrediting_provider.delete(provider.provider_name)
-
end
-
-
4
def publish_course
-
3
@course.publish_sites
-
3
@course.publish_enrichment(@current_user)
-
3
NotificationService::CoursePublished.call(course: @course)
-
end
-
-
4
def format_publish_error_messages
-
1
@course.errors.messages.transform_values do |error_messages|
-
2
error_messages.map { |message| message.gsub(/^\^/, "") }
-
end
-
end
-
end
-
end
-
1
module Publish
-
1
class NotificationsController < PublishController
-
1
skip_before_action :check_interrupt_redirects
-
-
1
def index
-
2
authorize(current_user, :index?)
-
-
2
@notifications_view = NotificationsView.new(request: request, current_user: current_user)
-
2
@notification_form = NotificationForm.new(current_user)
-
end
-
-
1
def update
-
2
authorize(current_user, :update?)
-
-
2
@notification_form = NotificationForm.new(current_user, params: notification_params)
-
-
2
if @notification_form.save!
-
1
flash[:success] = "Email notification preferences for #{current_user.email} have been saved."
-
-
1
redirect_to redirect_to_path
-
else
-
1
@notifications_view = NotificationsView.new(request: request, current_user: current_user)
-
1
render(:index)
-
end
-
end
-
-
1
private
-
-
1
def redirect_to_path
-
1
if permitted_params[:provider_code].present?
-
1
publish_provider_path(permitted_params[:provider_code])
-
else
-
root_path
-
end
-
end
-
-
1
def permitted_params
-
4
params.require(:publish_notification_form).permit(*NotificationForm::FIELDS, :provider_code)
-
end
-
-
1
def notification_params
-
2
permitted_params.except(:provider_code).transform_values do |value|
-
1
ActiveModel::Type::Boolean.new.cast(value)
-
end
-
end
-
end
-
end
-
module Publish
-
module Providers
-
class AccessRequestsController < PublishController
-
before_action :provider
-
-
def new
-
authorize AccessRequest
-
-
@access_request = AccessRequestForm.new(user: current_user)
-
end
-
-
def create
-
authorize AccessRequest
-
-
@access_request = AccessRequestForm.new(params: access_request_params, user: current_user)
-
-
if @access_request.save!
-
redirect_to users_publish_provider_path(params[:code]),
-
flash: { success: "Your request for access has been submitted" }
-
else
-
@errors = @access_request.errors.messages
-
-
render :new
-
end
-
end
-
-
private
-
-
def access_request_params
-
params.require(:publish_access_request_form).permit(
-
*AccessRequestForm::FIELDS,
-
)
-
end
-
end
-
end
-
end
-
3
module Publish
-
3
module Providers
-
3
class AllocationsController < PublishController
-
3
def index
-
29
authorize provider, :show?
-
-
29
@allocations_view = AllocationsView.new(
-
allocations: allocations[Settings.allocation_cycle_year.to_s] || [], training_providers: training_providers,
-
)
-
end
-
-
3
def new_repeat_request
-
2
authorize provider, :show?
-
2
provider
-
2
training_provider
-
2
@allocation = RepeatRequestForm.new
-
end
-
-
3
def edit
-
2
@allocation = Allocation.includes(:provider, :accredited_body)
-
.find(params[:id])
-
-
2
authorize @allocation
-
-
2
training_provider
-
end
-
-
3
def create
-
3
service = ::Allocations::Create.new(allocation_params.merge(accredited_body_id: provider.id, request_type: get_request_type(allocation_params)))
-
-
3
authorize service.object
-
-
3
@allocation = Publish::RepeatRequestForm.new(request_type: params[:request_type])
-
-
3
if @allocation.valid? && service.execute
-
3
redirect_to publish_provider_recruitment_cycle_allocation_path(id: service.object.id)
-
else
-
render :new_repeat_request
-
end
-
end
-
-
3
def update
-
2
@allocation = Allocation.find(params[:id])
-
-
2
@allocation.request_type = params[:request_type]
-
-
2
authorize @allocation
-
-
2
@allocation.save if @allocation.changed?
-
-
2
redirect_to publish_provider_recruitment_cycle_allocation_path(id: @allocation.id)
-
end
-
-
3
def show
-
6
@allocation = Allocation.find(params[:id])
-
6
provider
-
6
training_provider
-
-
6
authorize @allocation
-
end
-
-
3
def initial_request
-
35
authorize provider, :show?
-
-
35
provider
-
35
flow = InitialRequestFlow.new(params: params)
-
-
35
if request.post? && flow.valid? && flow.redirect?
-
5
redirect_to flow.redirect_path
-
else
-
30
render flow.template, locals: flow.locals
-
end
-
end
-
-
3
private
-
-
3
def training_provider
-
10
@training_provider ||= recruitment_cycle.providers.find_by(provider_code: params[:training_provider_code])
-
end
-
-
3
def allocation_params
-
6
params.slice(
-
:number_of_places,
-
:request_type,
-
).merge(
-
provider: recruitment_cycle.providers.find_by(provider_code: params[:training_provider_code]),
-
).to_unsafe_hash
-
end
-
-
3
def get_request_type(permitted_params)
-
3
return permitted_params[:request_type] if permitted_params[:request_type].present?
-
-
case permitted_params[:number_of_places]
-
when "0"
-
"declined"
-
when nil
-
"repeat"
-
else
-
"initial"
-
end
-
end
-
-
3
def previous_recruitment_cycle
-
58
@previous_recruitment_cycle ||= recruitment_cycle.previous
-
end
-
-
3
def allocations
-
58
@allocations ||= Allocation
-
.includes(:provider, :accredited_body, :allocation_uplift)
-
.where(accredited_body_code: provider.provider_code, recruitment_cycle: [previous_recruitment_cycle, recruitment_cycle])
-
.all
-
21
.group_by { |a| a.provider.recruitment_cycle_year }
-
end
-
-
3
def training_providers
-
29
@training_providers ||= (allocations[previous_recruitment_cycle.year.to_s] || []).filter_map { |a|
-
7
a.provider if a.request_type != AllocationsView::RequestType::DECLINED
-
}.sort_by(&:provider_name)
-
end
-
end
-
end
-
end
-
1
module Publish
-
1
module Providers
-
1
class ContactsController < PublishController
-
1
def edit
-
2
authorize(provider, :edit?)
-
-
2
@provider_contact_form = ProviderContactForm.new(provider)
-
end
-
-
1
def update
-
2
authorize(provider, :update?)
-
-
2
@provider_contact_form = ProviderContactForm.new(provider, params: provider_contact_params)
-
-
2
if @provider_contact_form.save!
-
1
flash[:success] = I18n.t("success.published")
-
-
1
redirect_to details_publish_provider_recruitment_cycle_path(
-
provider.provider_code,
-
recruitment_cycle.year,
-
)
-
else
-
1
render :edit
-
end
-
end
-
-
1
private
-
-
1
def provider_contact_params
-
2
params.require(:publish_provider_contact_form).permit(*ProviderContactForm::FIELDS)
-
end
-
end
-
end
-
end
-
1
module Publish
-
1
module Providers
-
1
class EditInitialAllocationsController < PublishController
-
1
def edit
-
27
authorize allocation
-
-
27
flow = EditInitialRequestFlow.new(params: params)
-
-
27
if request.post? && flow.valid?
-
8
redirect_to flow.redirect_path
-
else
-
19
render flow.template, locals: flow.locals
-
end
-
end
-
-
1
def update
-
1
authorize allocation
-
-
1
update_allocation
-
-
1
redirect_to publish_provider_recruitment_cycle_allocation_path(
-
provider_code: allocation.accredited_body.provider_code,
-
recruitment_cycle_year: recruitment_cycle.year,
-
training_provider_code: allocation.provider.provider_code,
-
id: allocation.id,
-
)
-
end
-
-
1
def delete
-
1
authorize allocation
-
-
1
allocation.destroy
-
-
1
redirect_to publish_provider_recruitment_cycle_allocation_confirm_deletion_path(
-
provider_code: provider.provider_code,
-
recruitment_cycle_year: recruitment_cycle.year,
-
training_provider_code: training_provider.provider_code,
-
)
-
end
-
-
1
def confirm_deletion
-
1
authorize provider, :show?
-
1
@allocation = Allocation.new(request_type: AllocationsView::RequestType::DECLINED)
-
-
1
training_provider
-
1
provider
-
1
recruitment_cycle
-
1
render template: "publish/providers/allocations/show"
-
end
-
-
1
private
-
-
1
def allocation
-
35
@allocation ||= Allocation.includes(:provider, :accredited_body)
-
.find(params[:id])
-
end
-
-
1
def update_allocation
-
1
allocation.number_of_places = params[:number_of_places].to_i
-
1
allocation.save
-
end
-
-
1
def training_provider
-
2
@training_provider ||= recruitment_cycle.providers.find_by(provider_code: params[:allocation_training_provider_code])
-
end
-
end
-
end
-
end
-
1
module Publish
-
1
module Providers
-
1
class LocationsController < PublishController
-
1
def index
-
3
authorize provider, :can_list_sites?
-
-
3
@locations = provider.sites.sort_by(&:location_name)
-
end
-
-
1
def new
-
1
authorize provider, :can_create_sites?
-
1
@location_form = LocationForm.new(provider.sites.new)
-
end
-
-
1
def create
-
1
authorize provider, :can_create_sites?
-
-
1
@location_form = LocationForm.new(provider.sites.new, params: site_params)
-
1
if @location_form.save!
-
1
flash[:success] = "Your location has been created"
-
1
redirect_to publish_provider_recruitment_cycle_locations_path(
-
@location_form.provider_code, @location_form.recruitment_cycle_year
-
)
-
else
-
render :new
-
end
-
end
-
-
1
def edit
-
1
authorize site, :update?
-
1
@location_form = LocationForm.new(site)
-
end
-
-
1
def update
-
1
authorize provider, :update?
-
1
@location_form = LocationForm.new(site, params: site_params)
-
-
1
if @location_form.save!
-
1
flash[:success] = I18n.t("success.value_published", value: "location details")
-
-
1
redirect_to publish_provider_recruitment_cycle_locations_path(
-
@location_form.provider_code, @location_form.recruitment_cycle_year
-
)
-
else
-
render :edit
-
end
-
end
-
-
1
private
-
-
1
def site
-
3
@site ||= provider.sites.find(params[:id])
-
end
-
-
1
def site_params
-
2
params.require(:publish_location_form).permit(LocationForm::FIELDS)
-
end
-
end
-
end
-
end
-
1
module Publish
-
1
module Providers
-
1
class VisasController < PublishController
-
1
def edit
-
2
authorize(provider, :edit?)
-
-
2
@provider_visa_form = ProviderVisaForm.new(provider)
-
end
-
-
1
def update
-
2
authorize(provider, :update?)
-
-
2
@provider_visa_form = ProviderVisaForm.new(provider, params: provider_visa_params)
-
-
2
if @provider_visa_form.save!
-
1
flash[:success] = I18n.t("success.published")
-
-
1
redirect_to details_publish_provider_recruitment_cycle_path(
-
provider.provider_code,
-
recruitment_cycle.year,
-
)
-
else
-
1
render :edit
-
end
-
end
-
-
1
private
-
-
1
def provider_visa_params
-
2
return { can_sponsor_student_visa: nil } if params[:publish_provider_visa_form].blank?
-
-
1
params.require(:publish_provider_visa_form).permit(*ProviderVisaForm::FIELDS).transform_values do |value|
-
2
ActiveModel::Type::Boolean.new.cast(value)
-
end
-
end
-
end
-
end
-
end
-
4
module Publish
-
4
class ProvidersController < PublishController
-
4
rescue_from ActiveRecord::RecordNotFound, with: :render_not_found
-
4
decorates_assigned :provider
-
-
4
def index
-
230
authorize :provider, :index?
-
-
230
page = (params[:page] || 1).to_i
-
230
per_page = 10
-
230
@pagy, @providers = pagy(providers.order(:provider_name), page: page, items: per_page)
-
-
230
render "publish/providers/no_providers", status: :forbidden if @providers.blank?
-
230
redirect_to publish_provider_path(@providers.first.provider_code) if @providers.count == 1
-
end
-
-
4
def suggest
-
authorize :provider, :show?
-
-
@provider_list = providers
-
.provider_search(params[:query])
-
.limit(10)
-
.map { |provider| { code: provider.provider_code, name: provider.provider_name } }
-
render json: @provider_list
-
end
-
-
4
def show
-
161
authorize provider
-
-
161
if FeatureService.enabled?(:new_publish_navigation)
-
7
redirect_to publish_provider_recruitment_cycle_courses_path(provider.provider_code, provider.recruitment_cycle_year)
-
else
-
154
:show?
-
end
-
end
-
-
4
def details
-
7
authorize provider, :show?
-
-
6
redirect_to_contact_page_with_ukprn_error if provider.ukprn.blank?
-
6
@errors = flash[:error_summary]
-
6
flash.delete(:error_summary)
-
end
-
-
4
def about
-
3
authorize provider, :show?
-
-
3
@about_form = AboutYourOrganisationForm.new(provider)
-
end
-
-
4
def update
-
4
authorize provider, :update?
-
-
4
@about_form = AboutYourOrganisationForm.new(provider, params: provider_params)
-
-
4
if @about_form.save!
-
3
flash[:success] = I18n.t("success.published")
-
3
redirect_to(
-
details_publish_provider_recruitment_cycle_path(
-
provider.provider_code,
-
provider.recruitment_cycle_year,
-
),
-
)
-
else
-
1
@errors = @about_form.errors.messages
-
1
render :about
-
end
-
end
-
-
4
def search
-
2
skip_authorization
-
-
2
provider_query = params[:query]
-
-
2
if provider_query.blank?
-
flash[:error] = { id: "provider-error", message: "Name or provider code" }
-
return redirect_to publish_root_path
-
end
-
-
2
provider_code = provider_query
-
.split
-
.last
-
.gsub(/[()]/, "")
-
-
2
redirect_to publish_provider_path(provider_code)
-
end
-
-
4
private
-
-
4
def provider
-
208
@provider ||= recruitment_cycle.providers.find_by!(provider_code: params[:provider_code] || params[:code])
-
end
-
-
4
def providers
-
230
@providers ||= if current_user.admin?
-
48
RecruitmentCycle.current.providers
-
else
-
182
RecruitmentCycle.current.providers.where(id: current_user.providers)
-
end
-
end
-
-
4
def redirect_to_contact_page_with_ukprn_error
-
flash[:error] = { id: "publish-provider-contact-form-ukprn-field", message: "Please enter a UKPRN before continuing" }
-
-
redirect_to contact_publish_provider_recruitment_cycle_path(provider.provider_code, provider.recruitment_cycle_year)
-
end
-
-
4
def provider_params
-
4
params
-
.fetch(:publish_about_your_organisation_form, {})
-
.permit(
-
*AboutYourOrganisationForm::FIELDS,
-
accredited_bodies: %i[provider_name provider_code description],
-
)
-
end
-
end
-
end
-
4
module Publish
-
4
class PublishController < ApplicationController
-
4
include SuccessMessage
-
-
4
layout "publish"
-
-
4
before_action :check_interrupt_redirects
-
-
4
after_action :verify_authorized
-
-
4
private
-
-
4
def provider
-
590
@provider ||= recruitment_cycle.providers.find_by(provider_code: params[:provider_code] || params[:code])
-
end
-
-
4
def recruitment_cycle
-
698
cycle_year = params[:recruitment_cycle_year] || params[:year] || Settings.current_recruitment_cycle_year
-
-
698
@recruitment_cycle ||= RecruitmentCycle.find_by!(year: cycle_year)
-
end
-
-
4
def show_errors_on_publish?
-
27
params[:display_errors].present?
-
end
-
-
4
def check_interrupt_redirects(use_redirect_back_to: true)
-
886
if !current_user.accepted_terms?
-
8
redirect_to publish_accept_terms_path
-
878
elsif show_rollover_page?
-
2
redirect_to publish_rollover_path
-
876
elsif show_rollover_recruitment_page?
-
2
redirect_to publish_rollover_recruitment_path
-
874
elsif use_redirect_back_to
-
874
redirect_to session[:redirect_back_to] if session[:redirect_back_to].present?
-
874
session.delete(:redirect_back_to)
-
end
-
end
-
-
4
def show_rollover_page?
-
878
FeatureService.enabled?("rollover.can_edit_current_and_next_cycles") && current_user.current_rollover_acceptance.blank?
-
end
-
-
4
def show_rollover_recruitment_page?
-
876
FeatureService.enabled?("rollover.show_next_cycle_allocation_recruitment_page") &&
-
current_user.current_rollover_recruitment_acceptance.blank?
-
end
-
end
-
end
-
module Publish
-
class RecruitmentCyclesController < PublishController
-
def show
-
authorize provider, :show?
-
-
@recruitment_cycle = RecruitmentCycle.find_by(year: params[:year])
-
@provider ||= recruitment_cycle.providers.find_by!(provider_code: params[:provider_code] || params[:code])
-
-
unless @provider.rolled_over?
-
redirect_to publish_provider_path(@provider.provider_code)
-
end
-
end
-
end
-
end
-
1
module Publish
-
1
class RolloverController < ApplicationController
-
1
def new; end
-
-
1
def create
-
1
InterruptPageAcknowledgement.find_or_create_by!(
-
user: current_user,
-
recruitment_cycle: RecruitmentCycle.current,
-
page: "rollover",
-
)
-
-
1
redirect_to publish_root_path
-
end
-
end
-
end
-
1
module Publish
-
1
class RolloverRecruitmentController < ApplicationController
-
1
def new; end
-
-
1
def create
-
1
InterruptPageAcknowledgement.find_or_create_by!(
-
user: current_user,
-
recruitment_cycle: RecruitmentCycle.current,
-
page: "rollover_recruitment",
-
)
-
-
1
redirect_to publish_root_path
-
end
-
end
-
end
-
2
module Publish
-
2
class TermsController < ApplicationController
-
2
skip_before_action :authenticate
-
-
2
def edit
-
8
@accept_terms_form = Interruption::AcceptTermsForm.new(current_user)
-
end
-
-
2
def update
-
4
@accept_terms_form = Interruption::AcceptTermsForm.new(current_user, params: accept_term_params)
-
-
4
if @accept_terms_form.save!
-
2
redirect_to publish_root_path
-
else
-
2
render :edit
-
end
-
end
-
-
2
private
-
-
2
def accept_term_params
-
4
params.require(:publish_interruption_accept_terms_form).permit(*Interruption::AcceptTermsForm::FIELDS)
-
end
-
end
-
end
-
module Publish
-
module TrainingProviders
-
class CourseExportsController < PublishController
-
def index
-
authorize(provider, :can_list_training_providers?)
-
-
respond_to do |format|
-
format.csv do
-
send_data(data_export.data, filename: data_export.filename, disposition: :attachment)
-
end
-
end
-
end
-
-
private
-
-
def courses
-
@courses ||= provider.current_accredited_courses.includes(:enrichments, :sites, :site_statuses)
-
end
-
-
def data_export
-
@data_export ||= Exports::AccreditedCourseList.new(courses)
-
end
-
end
-
end
-
end
-
1
module Publish
-
1
module TrainingProviders
-
1
class CoursesController < PublishController
-
1
def index
-
1
authorize(provider, :index?)
-
-
1
@courses = fetch_courses
-
end
-
-
1
private
-
-
1
def training_provider
-
1
@training_provider ||= provider.training_providers.find_by(provider_code: params[:training_provider_code])
-
end
-
-
1
def fetch_courses
-
1
training_provider
-
.courses
-
.includes(:enrichments, :site_statuses, provider: [:recruitment_cycle])
-
.where(accredited_body_code: provider.provider_code)
-
.order(:name)
-
.map(&:decorate)
-
end
-
end
-
end
-
end
-
1
module Publish
-
1
class TrainingProvidersController < PublishController
-
1
def index
-
2
authorize(provider, :can_list_training_providers?)
-
-
2
@training_providers = provider.training_providers.include_accredited_courses_counts(provider.provider_code).order(:provider_name)
-
4
@course_counts = @training_providers.to_h { |p| [p.provider_code, p.accredited_courses_count] }
-
end
-
end
-
end
-
module Publish
-
class UsersController < PublishController
-
def index
-
authorize(provider)
-
-
@users = provider.users
-
end
-
end
-
end
-
1
class ReportingController < ActionController::API
-
1
before_action :build_recruitment_cycle
-
-
1
def reporting
-
1
render json: StatisticService.reporting(recruitment_cycle: @recruitment_cycle)
-
end
-
-
1
private
-
-
1
def build_recruitment_cycle
-
1
@recruitment_cycle = RecruitmentCycle.find_by(
-
year: params[:recruitment_cycle_year],
-
) || RecruitmentCycle.current_recruitment_cycle
-
end
-
end
-
4
class SessionsController < ApplicationController
-
4
skip_before_action :authenticate
-
-
4
def sign_out
-
1
if AuthenticationService.persona?
-
redirect_to "/auth/developer/signout"
-
else
-
1
redirect_to "/auth/dfe/signout"
-
end
-
end
-
-
4
def callback
-
225
UserSession.begin_session!(session, request.env["omniauth.auth"])
-
-
225
if current_user
-
223
UserSessions::Update.call(user: current_user, user_session: user_session)
-
-
223
redirect_to after_sign_in_path
-
else
-
2
UserSession.end_session!(session)
-
-
2
redirect_to user_not_found_path
-
end
-
end
-
-
4
def destroy
-
2
if current_user.present?
-
1
UserSession.end_session!(session)
-
1
redirect_to user_session.logout_url
-
else
-
1
redirect_to support_providers_path
-
end
-
end
-
-
4
private
-
-
4
def after_sign_in_path
-
223
saved_path = session.delete("post_dfe_sign_in_path")
-
-
223
saved_path || publish_root_path
-
end
-
end
-
4
class SignInController < ApplicationController
-
4
skip_before_action :authenticate
-
-
4
def index
-
228
if AuthenticationService.magic_link?
-
4
redirect_to magic_links_path
-
else
-
224
render "index"
-
end
-
end
-
end
-
2
module Support
-
2
class AllocationUpliftsController < SupportController
-
2
def edit
-
1
allocation_uplift
-
end
-
-
2
def new
-
1
@allocation_uplift = AllocationUplift.new(allocation: allocation)
-
end
-
-
2
def create
-
1
@allocation_uplift = AllocationUplift.new(create_allocation_uplift_params)
-
1
@allocation_uplift.allocation = allocation
-
1
if @allocation_uplift.save
-
1
redirect_to support_allocation_path(@allocation_uplift.allocation), flash: { success: "Allocation Uplift was successfully created" }
-
else
-
render :new
-
end
-
end
-
-
2
def update
-
1
if allocation_uplift.update(update_allocation_uplift_params)
-
1
redirect_to support_allocation_path(allocation_uplift.allocation), flash: { success: "Allocation Uplift was successfully updated" }
-
else
-
render :edit
-
end
-
end
-
-
2
private
-
-
2
def allocation_uplift
-
3
@allocation_uplift ||= allocation.allocation_uplift
-
end
-
-
2
def allocation
-
4
@allocation ||= Allocation.find(params[:allocation_id])
-
end
-
-
2
def create_allocation_uplift_params
-
1
params.require(:allocation_uplift).permit(:allocation_id, :uplifts)
-
end
-
-
2
def update_allocation_uplift_params
-
1
params.require(:allocation_uplift).permit(:uplifts)
-
end
-
end
-
end
-
3
module Support
-
3
class AllocationsController < SupportController
-
3
def index
-
6
@allocations = filtered_allocations
-
end
-
-
3
def show
-
3
allocation
-
end
-
-
3
private
-
-
3
def allocation
-
3
@allocation ||= Allocation.find(params[:id])
-
end
-
-
3
def filtered_allocations
-
6
@filtered_allocations ||= Support::Filter.call(model_data_scope: Allocation.current_allocations, filter_params: filter_params)
-
end
-
-
3
def filter_params
-
6
@filter_params ||= params.except(:commit).permit(:text_search, :page)
-
end
-
end
-
end
-
3
module Support
-
3
class CoursesController < SupportController
-
3
def index
-
7
@courses = provider.courses.order(:name).page(params[:page] || 1)
-
6
render layout: "provider_record"
-
rescue ActiveRecord::RecordNotFound
-
1
flash[:warning] = "Provider not found"
-
1
redirect_to support_providers_path
-
end
-
-
3
def edit
-
9
@edit_course_form = Support::EditCourseForm.new(course)
-
end
-
-
3
def update
-
4
@edit_course_form = Support::EditCourseForm.new(course)
-
4
@edit_course_form.assign_attributes(update_course_params)
-
-
4
if @edit_course_form.save
-
1
redirect_to support_provider_courses_path(provider), flash: { success: t("support.flash.updated", resource: "Course") }
-
else
-
3
render :edit
-
end
-
end
-
-
3
private
-
-
3
def provider
-
21
@provider ||= RecruitmentCycle.current.providers.find(params[:provider_id])
-
end
-
-
3
def course
-
13
@course ||= provider.courses.find(params[:id])
-
end
-
-
3
def update_course_params
-
4
params.require(:support_edit_course_form).permit(
-
*EditCourseForm::FIELDS,
-
:"start_date(3i)", :"start_date(2i)", :"start_date(1i)",
-
:"applications_open_from(3i)", :"applications_open_from(2i)", :"applications_open_from(1i)",
-
:is_send
-
36
).transform_keys { |key| date_field_to_attribute(key) }
-
end
-
-
3
def date_field_to_attribute(key)
-
36
case key
-
4
when "start_date(3i)" then "start_date_day"
-
4
when "start_date(2i)" then "start_date_month"
-
4
when "start_date(1i)" then "start_date_year"
-
4
when "applications_open_from(3i)" then "applications_open_from_day"
-
4
when "applications_open_from(2i)" then "applications_open_from_month"
-
4
when "applications_open_from(1i)" then "applications_open_from_year"
-
12
else key
-
end
-
end
-
end
-
end
-
module Support
-
class DataExportsController < SupportController
-
def index
-
@data_exports = DataExports::DataExport.all
-
end
-
-
def download
-
unless (@data_export = DataExports::DataExport.find(params[:id]))
-
redirect_to action: :index, error: "Unable to find data export"
-
return
-
end
-
send_data @data_export.to_csv, filename: @data_export.filename, disposition: :attachment
-
end
-
end
-
end
-
1
module Support
-
1
class LocationsController < SupportController
-
1
def index
-
8
@sites = provider.sites.order(:location_name).page(params[:page] || 1)
-
8
render layout: "provider_record"
-
rescue ActiveRecord::RecordNotFound
-
flash[:warning] = "Provider not found"
-
redirect_to support_providers_path
-
end
-
-
1
def new
-
1
@site = provider.sites.build
-
end
-
-
1
def create
-
1
@site = provider.sites.build(site_params)
-
-
1
if @site.save
-
1
redirect_to support_provider_locations_path(provider), flash: { success: t("support.flash.created", resource: flash_resource) }
-
else
-
render :new
-
end
-
end
-
-
1
def edit
-
3
provider
-
3
site
-
end
-
-
1
def update
-
2
if site.update(site_params)
-
1
redirect_to support_provider_locations_path(provider), flash: { success: t("support.flash.updated", resource: flash_resource) }
-
else
-
1
render :edit
-
end
-
end
-
-
1
def destroy
-
1
site.destroy!
-
-
1
redirect_to support_provider_locations_path(provider), flash: { success: t("support.flash.deleted", resource: flash_resource) }
-
end
-
-
1
private
-
-
1
def provider
-
22
@provider ||= RecruitmentCycle.current.providers.find(params[:provider_id])
-
end
-
-
1
def flash_resource
-
3
@flash_resource ||= "Location"
-
end
-
-
1
def site_params
-
3
params.require(:site).permit(
-
:location_name,
-
:urn,
-
:code,
-
:address1,
-
:address2,
-
:address3,
-
:address4,
-
:postcode,
-
)
-
end
-
-
1
def site
-
6
@site ||= provider.sites.find(params[:id])
-
end
-
end
-
end
-
4
module Support
-
4
class ProvidersController < SupportController
-
4
def index
-
21
@providers = filtered_providers
-
end
-
-
4
def new
-
2
@provider = Provider.new
-
2
@provider.sites.build
-
2
@provider.organisations.build
-
end
-
-
4
def create
-
2
@provider = Provider.new(create_provider_params)
-
2
if @provider.save
-
1
redirect_to support_provider_path(provider), flash: { success: "Provider was successfully created" }
-
else
-
# The below code is to fix a mismatch of error messages
-
# for invalid forms in the support console.
-
1
@provider.errors.messages.each { |k, v|
-
8
case k
-
when :"sites.urn", :email, :telephone
-
2
@provider.errors.messages[k] = v.first.gsub("^", "")
-
else
-
6
@provider.errors.messages[k] = "#{k.to_s.gsub(/.\.^?/, ' ').humanize} #{v.first}"
-
end
-
}
-
1
render :new
-
end
-
end
-
-
4
def show
-
8
provider
-
8
render layout: "provider_record"
-
end
-
-
4
def edit
-
4
provider
-
end
-
-
4
def update
-
2
if provider.update(update_provider_params)
-
1
redirect_to support_provider_path(provider), flash: { success: t("support.flash.updated", resource: "Provider") }
-
else
-
1
render :edit
-
end
-
end
-
-
4
def users
-
4
@users = provider.users.order(:last_name).page(params[:page] || 1)
-
4
render layout: "provider_record"
-
end
-
-
4
private
-
-
4
def filtered_providers
-
21
@filtered_providers ||= Support::Filter.call(model_data_scope: find_providers, filter_params: filter_params)
-
end
-
-
4
def find_providers
-
21
RecruitmentCycle.current.providers.order(:provider_name).includes(:courses, :users)
-
end
-
-
4
def filter_params
-
21
@filter_params ||= params.except(:commit).permit(:provider_search, :course_search, :page)
-
end
-
-
4
def provider
-
20
@provider ||= Provider.find(params[:id])
-
end
-
-
4
def update_provider_params
-
2
params.require(:provider).permit(:provider_name, :provider_type)
-
end
-
-
4
def create_provider_params
-
2
params.require(:provider).permit(:provider_name,
-
:provider_code,
-
:provider_type,
-
:urn,
-
:recruitment_cycle_id,
-
:email,
-
:ukprn,
-
:telephone, sites_attributes: %i[code
-
urn
-
location_name
-
address1
-
address2
-
address3
-
address4
-
postcode],
-
organisations_attributes: %i[name]).merge(recruitment_cycle: RecruitmentCycle.current_recruitment_cycle)
-
end
-
end
-
end
-
4
module Support
-
4
class SupportController < ApplicationController
-
4
layout "support"
-
4
before_action :check_user_is_admin
-
-
4
private
-
-
4
def check_user_is_admin
-
122
if !current_user.admin?
-
1
flash[:warning] = "User is not an admin"
-
1
redirect_to sign_in_path
-
end
-
end
-
end
-
end
-
1
module Support
-
1
class UserPermissionsController < SupportController
-
1
def destroy
-
2
user.remove_access_to(provider)
-
-
2
redirect_to origin_page, flash: { success: t("support.flash.deleted", resource: flash_resource) }
-
end
-
-
1
private
-
-
1
def user_permission
-
4
@user_permission ||= UserPermission.find(params[:id])
-
end
-
-
1
def user
-
2
@user ||= user_permission.user
-
end
-
-
1
def provider
-
2
@provider ||= user_permission.provider
-
end
-
-
1
def origin_page
-
2
request.referer
-
end
-
-
1
def flash_resource
-
2
@flash_resource ||= "User permission"
-
end
-
end
-
end
-
1
module Support
-
1
module Users
-
1
class ProvidersController < SupportController
-
1
def show
-
2
user
-
2
@providers = providers.order(:provider_name).page(params[:page] || 1)
-
2
render layout: "user_record"
-
end
-
-
1
private
-
-
1
def user
-
4
@user ||= User.find(params[:user_id])
-
end
-
-
1
def providers
-
2
RecruitmentCycle.current.providers.where(id: user.providers)
-
end
-
end
-
end
-
end
-
2
module Support
-
2
class UsersController < SupportController
-
2
def index
-
15
@users = filtered_users.page(params[:page] || 1)
-
end
-
-
2
def show
-
3
user
-
3
render layout: "user_record"
-
end
-
-
2
def new
-
4
@user = User.new
-
end
-
-
2
def create
-
2
@user = User.new(user_params)
-
2
if @user.save
-
1
redirect_to support_users_path
-
else
-
1
render :new
-
end
-
end
-
-
2
def edit
-
user
-
end
-
-
2
def update
-
if user.update(update_user_params)
-
redirect_to support_user_path(user), flash: { success: t("support.flash.updated", resource: "User") }
-
else
-
render :edit
-
end
-
end
-
-
2
def destroy
-
1
if user.discard
-
1
redirect_to support_users_path, flash: { success: "User successfully deleted" }
-
else
-
redirect_to support_users_path, flash: { success: "This user has already been deleted" }
-
end
-
end
-
-
2
private
-
-
2
def user_params
-
2
params.require(:user).permit(:first_name, :last_name, :email).merge(state: "new")
-
end
-
-
2
def update_user_params
-
params.require(:user).permit(:first_name, :last_name, :email, :admin)
-
end
-
-
2
def user
-
4
@user ||= User.find(params[:id])
-
end
-
-
2
def filtered_users
-
15
Support::Filter.call(model_data_scope: User.order(:last_name), filter_params: filter_params)
-
end
-
-
2
def filter_params
-
15
@filter_params ||= params.except(:commit).permit(:text_search, :page, :commit, user_type: [])
-
end
-
end
-
end
-
4
class ApplicationDecorator < Draper::Decorator
-
# TODO: Move this to a view component
-
4
def status_tag
-
45
tag = h.govuk_tag(text: status_text.html_safe, colour: status_colour)
-
45
tag += unpublished_status_hint if object.has_unpublished_changes?
-
45
tag.html_safe
-
end
-
-
4
private
-
-
4
def status_text
-
45
return status_tags[:withdrawn][:text] if object.ucas_status == "not_running"
-
-
45
status_tags[object.content_status.to_sym][:text]
-
end
-
-
4
def status_colour
-
45
return status_tags[:withdrawn][:colour] if object.ucas_status == "not_running"
-
-
45
status_tags[object.content_status.to_sym][:colour]
-
end
-
-
4
def status_tags
-
{
-
90
published: { text: "Published", colour: "green" },
-
withdrawn: { text: "Withdrawn", colour: "red" },
-
empty: { text: "Empty", colour: "grey" },
-
draft: { text: "Draft", colour: "yellow" },
-
published_with_unpublished_changes: { text: "Published *", colour: "green" },
-
rolled_over: { text: "Rolled over", colour: "grey" },
-
}
-
end
-
-
4
def unpublished_status_hint
-
4
h.tag.span("* Unpublished changes".html_safe, class: "govuk-body-s govuk-!-display-block govuk-!-margin-bottom-0 govuk-!-margin-top-1")
-
end
-
end
-
4
class CourseDecorator < ApplicationDecorator
-
4
delegate_all
-
-
4
def name_and_code
-
260
"#{object.name} (#{object.course_code})"
-
end
-
-
4
def vacancies
-
17
content = object.has_vacancies? ? "Yes" : "No"
-
17
content += " (#{edit_vacancy_link})" unless object.is_withdrawn?
-
17
content.html_safe
-
end
-
-
4
def find_url(provider = object.provider)
-
4
h.search_ui_course_page_url(provider_code: provider.provider_code, course_code: object.course_code)
-
end
-
-
4
def on_find(provider = object.provider)
-
45
if object.findable?
-
13
if current_cycle_and_open?
-
13
h.govuk_link_to("Yes - view online", h.search_ui_course_page_url(provider_code: provider.provider_code, course_code: object.course_code))
-
else
-
"Yes - from #{Date::MONTHNAMES[Settings.next_cycle_open_date.to_date.month]}"
-
end
-
else
-
32
not_on_find
-
end
-
end
-
-
4
def open_or_closed_for_applications
-
14
object.open_for_applications? ? "Open" : "Closed"
-
end
-
-
4
def outcome
-
21
I18n.t("edit_options.qualifications.#{object.qualification}.label")
-
end
-
-
4
def is_send?
-
16
object.is_send? ? "Yes" : "No"
-
end
-
-
4
def funding
-
{
-
14
"salary" => "Salaried",
-
"apprenticeship" => "Teaching apprenticeship (with salary)",
-
"fee" => "Fee paying (no salary)",
-
}[object.funding_type]
-
end
-
-
4
def subject_name
-
3
if object.subjects.size == 1
-
2
object.subjects.first.subject_name
-
else
-
1
object.name
-
end
-
end
-
-
4
def has_scholarship_and_bursary?
-
3
object.has_bursary? && object.has_scholarship?
-
end
-
-
4
def bursary_first_line_ending
-
if bursary_requirements.count > 1
-
":"
-
else
-
"#{bursary_requirements.first}."
-
end
-
end
-
-
# def bursary_requirements
-
# requirements = ["a degree of 2:2 or above in any subject"]
-
-
# if object.subjects.any? { |subject| subject.subject_name.downcase == "primary with mathematics" }
-
# mathematics_requirement = "at least grade B in maths A-level (or an equivalent)"
-
# requirements.push(mathematics_requirement)
-
# end
-
-
# requirements
-
# end
-
-
4
def bursary_only?
-
1
object.has_bursary? && !object.has_scholarship?
-
end
-
-
4
def excluded_from_bursary?
-
3
object.subjects.present? &&
-
# incorrect bursary eligibility only shows up on courses with 2 subjects
-
object.subjects.count == 2 &&
-
has_excluded_course_name?
-
end
-
-
# def has_early_career_payments?
-
# object.subjects.present? &&
-
# object.subjects.any? { |subject| subject.attributes["early_career_payments"].present? }
-
# end
-
-
# def bursary_amount
-
# find_max("bursary_amount")
-
# end
-
-
# def scholarship_amount
-
# find_max("scholarship")
-
# end
-
-
4
def salaried?
-
9
object.funding_type == "salary" || object.funding_type == "apprenticeship"
-
end
-
-
4
def apprenticeship?
-
2
object.funding_type.to_s == "apprenticeship" ? "Yes" : "No"
-
end
-
-
4
def sorted_subjects
-
16
object.subjects.map(&:subject_name).sort.join("<br>").html_safe
-
end
-
-
4
def length
-
29
case course_length.to_s
-
when "OneYear"
-
1
"1 year"
-
when "TwoYears"
-
7
"Up to 2 years"
-
else
-
21
course_length.to_s
-
end
-
end
-
-
4
def other_course_length?
-
3
%w[OneYear TwoYears].exclude?(course_length) && !course_length.nil?
-
end
-
-
4
def other_age_range?
-
36
options = object.edit_course_options["age_range_in_years"]
-
36
options.exclude?(course.age_range_in_years)
-
end
-
-
4
def alphabetically_sorted_sites
-
1
object.sites.sort_by(&:location_name)
-
end
-
-
4
def preview_site_statuses
-
11
object.site_statuses.new_or_running.sort_by { |status| status.site.location_name }
-
end
-
-
4
def has_site?(site)
-
11
!course.sites.nil? && object.sites.any? { |s| s.id == site.id }
-
end
-
-
# rubocop:disable Lint/DuplicateBranch: Duplicate branch body detected
-
4
def funding_option
-
3
if salaried?
-
1
"Salary"
-
2
elsif excluded_from_bursary?
-
# Duplicate branch body detected
-
"Student finance if you’re eligible"
-
2
elsif has_scholarship_and_bursary?
-
2
"Scholarships or bursaries, as well as student finance, are available if you’re eligible"
-
elsif has_bursary?
-
"Bursaries and student finance are available if you’re eligible"
-
else
-
# Duplicate branch body detected
-
"Student finance if you’re eligible"
-
end
-
end
-
# rubocop:enable Lint/DuplicateBranch: Duplicate branch body detected
-
-
4
def current_cycle?
-
21
course.recruitment_cycle.year.to_i == Settings.current_recruitment_cycle_year
-
end
-
-
4
def current_cycle_and_open?
-
13
current_cycle? && FeatureService.enabled?("rollover.has_current_cycle_started?")
-
end
-
-
4
def next_cycle?
-
17
course.recruitment_cycle.year.to_i == Settings.current_recruitment_cycle_year + 1
-
end
-
-
4
def use_financial_support_placeholder?
-
2
course.recruitment_cycle.year.to_i == Settings.financial_support_placeholder_cycle
-
end
-
-
4
def cycle_range
-
2
"#{course.recruitment_cycle.year} to #{course.recruitment_cycle.year.to_i + 1}"
-
end
-
-
4
def age_range
-
15
if object.age_range_in_years.present?
-
15
I18n.t("edit_options.age_range_in_years.#{object.age_range_in_years}.label", default: object.age_range_in_years.humanize)
-
else
-
"<span class='app-!-colour-muted'>Unknown</span>".html_safe
-
end
-
end
-
-
4
def applications_open_from_message_for(recruitment_cycle)
-
6
if current_cycle?
-
6
"As soon as the course is on Find (recommended)"
-
else
-
year = recruitment_cycle.year.to_i
-
day_month = recruitment_cycle.application_start_date.strftime("%-d %B")
-
"On #{day_month} when applications for the #{year} to #{year + 1} cycle open"
-
end
-
end
-
-
4
def selectable_subjects
-
356
edit_course_options["subjects"].map { |subject| [subject.attributes["subject_name"], subject["id"]] }
-
end
-
-
4
def selected_subject_ids
-
29
selectable_subject_ids = course.subjects.map { |subject| subject["id"] }
-
22
selected_subject_ids = subjects.map(&:id)
-
-
22
selectable_subject_ids & selected_subject_ids
-
end
-
-
4
def subject_present?(subject_to_find)
-
74
subjects.any? do |course_subject|
-
75
course_subject.id == subject_to_find.id
-
end
-
end
-
-
4
def return_start_date
-
7
if FeatureService.enabled?("rollover.can_edit_current_and_next_cycles")
-
1
start_date.presence || "September #{Settings.current_recruitment_cycle_year + 1}"
-
else
-
6
start_date.presence || "September #{Settings.current_recruitment_cycle_year}"
-
end
-
end
-
-
4
def placements_heading
-
60
if is_further_education?
-
1
"How teaching placements work"
-
else
-
59
"How school placements work"
-
end
-
end
-
-
4
def listing_basic_details
-
13
if is_further_education?
-
1
["outcome",
-
"full time or part time",
-
"fee or salary",
-
"application open date",
-
"course start date"]
-
else
-
12
["age range",
-
"outcome",
-
"full time or part time",
-
"application open date",
-
"course start date",
-
"GCSE requirements"]
-
end
-
end
-
-
4
def subject_page_title
-
24
case level
-
when "primary"
-
14
"Pick a primary subject"
-
when "secondary"
-
9
"Pick a secondary subject"
-
else
-
1
"Pick a subject"
-
end
-
end
-
-
4
def subject_input_label
-
15
case level
-
when "primary"
-
8
"Primary subject"
-
when "secondary"
-
6
"Secondary subject"
-
else
-
1
"Pick a subject"
-
end
-
end
-
-
# def accept_gcse_equivalency?
-
# object.accept_gcse_equivalency
-
# end
-
-
4
def has_fees?
-
50
object.funding_type.match?(/fee/)
-
end
-
-
4
def is_further_education?
-
91
object.level.match?(/further_education/)
-
end
-
-
4
def degree_section_complete?
-
61
object.degree_grade.present?
-
end
-
-
4
def gcse_section_complete?
-
64
!object.accept_pending_gcse.nil? && !object.accept_gcse_equivalency.nil?
-
end
-
-
4
def about_course
-
28
object.enrichment_attribute(:about_course)
-
end
-
-
4
def interview_process
-
30
object.enrichment_attribute(:interview_process)
-
end
-
-
4
def how_school_placements_work
-
28
object.enrichment_attribute(:how_school_placements_work)
-
end
-
-
4
def fee_uk_eu
-
3
object.enrichment_attribute(:fee_uk_eu)
-
end
-
-
4
def fee_international
-
3
object.enrichment_attribute(:fee_international)
-
end
-
-
4
def fee_details
-
4
object.enrichment_attribute(:fee_details)
-
end
-
-
4
def financial_support
-
4
object.enrichment_attribute(:financial_support)
-
end
-
-
4
def salary_details
-
24
object.enrichment_attribute(:salary_details)
-
end
-
-
4
def personal_qualities
-
28
object.enrichment_attribute(:personal_qualities)
-
end
-
-
4
def other_requirements
-
28
object.enrichment_attribute(:other_requirements)
-
end
-
-
4
def course_length
-
55
object.enrichment_attribute(:course_length)
-
end
-
-
4
def about_accrediting_body
-
4
object.accrediting_provider_description
-
end
-
-
4
def has_physical_education_subject?
-
subjects.map(&:subject_name).include?("Physical education")
-
end
-
-
4
private
-
-
4
def not_on_find
-
32
if object.new_and_not_running?
-
31
"No - still in draft"
-
1
elsif object.is_withdrawn?
-
1
"No - withdrawn"
-
else
-
"No"
-
end
-
end
-
-
4
def edit_vacancy_link
-
14
h.govuk_link_to(h.vacancies_publish_provider_recruitment_cycle_course_path(object.provider_code, object.recruitment_cycle.year, object.course_code)) do
-
14
h.raw("Change<span class=\"govuk-visually-hidden\"> vacancies for #{name_and_code}</span>")
-
end
-
end
-
-
# def find_max(attribute)
-
# subject_attributes = object.subjects.map do |s|
-
# if s.attributes[attribute].present?
-
# s.__send__(attribute).to_i
-
# end
-
# end
-
-
# subject_attributes.compact.max.to_s
-
# end
-
-
4
def has_excluded_course_name?
-
exclusions = [
-
/^Drama/,
-
/^Media Studies/,
-
/^PE/,
-
/^Physical/,
-
]
-
# We only care about course with a name matching the pattern 'Foo with bar'
-
# We don't care about courses matching the pattern 'Foo and bar'
-
return false unless /with/.match?(object.name)
-
-
exclusions.any? { |e| e.match?(object.name) }
-
end
-
end
-
class ProviderDecorator < ApplicationDecorator
-
delegate_all
-
-
def accredited_bodies
-
object.accredited_bodies.sort_by { |provider| provider["provider_name"] }.map do |provider|
-
Struct.new(:provider_name, :provider_code, :description, keyword_init: true).new(provider)
-
end
-
end
-
-
def website
-
return if object.website.blank?
-
-
object.website.start_with?("http") ? object.website : "http://#{object.website}"
-
end
-
end
-
module API
-
module V3
-
class DeserializableAccessRequest < JSONAPI::Deserializable::Resource
-
attributes :first_name,
-
:last_name,
-
:email_address,
-
:organisation,
-
:reason,
-
:requester_email
-
end
-
end
-
end
-
module API
-
module V3
-
class DeserializableContact < JSONAPI::Deserializable::Resource
-
attributes :name, :email, :telephone, :permission_given, :type
-
end
-
end
-
end
-
1
module API
-
1
module V3
-
1
class DeserializableCourse < JSONAPI::Deserializable::Resource
-
1
COURSE_ATTRIBUTES = %i[
-
about_course
-
course_length
-
fee_details
-
fee_international
-
fee_uk_eu
-
financial_support
-
how_school_placements_work
-
interview_process
-
other_requirements
-
personal_qualities
-
salary_details
-
required_qualifications
-
course_code
-
name
-
study_mode
-
qualifications
-
english
-
maths
-
science
-
qualification
-
age_range_in_years
-
start_date
-
applications_open_from
-
study_mode
-
is_send
-
accredited_body_code
-
funding_type
-
level
-
degree_grade
-
additional_degree_subject_requirements
-
degree_subject_requirements
-
accept_pending_gcse
-
accept_gcse_equivalency
-
accept_english_gcse_equivalency
-
accept_maths_gcse_equivalency
-
accept_science_gcse_equivalency
-
additional_gcse_equivalencies
-
].freeze
-
-
1
attributes(*COURSE_ATTRIBUTES)
-
-
1
has_many :sites
-
1
has_many :subjects
-
-
1
def reverse_mapping
-
1
declared_attributes = DeserializableCourse.attr_blocks.keys
-
1
declared_attributes
-
36
.to_h { |key| [key.to_sym, "/data/attributes/#{key}"] }
-
end
-
end
-
end
-
end
-
1
module API
-
1
module V3
-
1
class DeserializableProvider < JSONAPI::Deserializable::Resource
-
1
PROVIDER_ATTRIBUTES = %i[
-
train_with_us
-
train_with_disability
-
provider_name
-
email
-
telephone
-
website
-
address1
-
address2
-
address3
-
address4
-
postcode
-
region_code
-
accredited_bodies
-
admin_contact
-
utt_contact
-
web_link_contact
-
fraud_contact
-
finance_contact
-
type_of_gt12
-
gt12_contact
-
send_application_alerts
-
application_alert_contact
-
ukprn
-
urn
-
can_sponsor_skilled_worker_visa
-
can_sponsor_student_visa
-
].freeze
-
-
1
attributes(*PROVIDER_ATTRIBUTES)
-
-
1
has_many :sites
-
1
has_many :courses
-
-
1
def reverse_mapping
-
1
declared_attributes = DeserializableProvider.attr_blocks.keys
-
1
declared_attributes
-
26
.to_h { |key| [key.to_sym, "/data/attributes/#{key}"] }
-
end
-
end
-
end
-
end
-
1
module API
-
1
module V3
-
1
class DeserializableSite < JSONAPI::Deserializable::Resource
-
1
attributes :address1,
-
:address2,
-
:address3,
-
:address4,
-
:code,
-
:location_name,
-
:postcode,
-
:region_code,
-
:urn
-
end
-
end
-
end
-
1
class EditInitialRequestFlow
-
1
include Rails.application.routes.url_helpers
-
-
1
attr_reader :params
-
-
1
delegate :valid?, to: :form_object
-
-
1
def initialize(params:)
-
27
@params = params
-
end
-
-
1
def template
-
19
if can_proceed_to_check_answers_page?
-
2
"publish/providers/allocations/edit_initial_allocations/check_answers"
-
17
elsif can_proceed_to_number_of_places_page?
-
10
"publish/providers/allocations/edit_initial_allocations/number_of_places"
-
else
-
7
"publish/providers/allocations/edit_initial_allocations/do_you_want"
-
end
-
end
-
-
1
def locals
-
{
-
19
training_provider: training_provider,
-
provider: provider,
-
form_object: form_object,
-
recruitment_cycle: recruitment_cycle,
-
allocation: allocation,
-
}
-
end
-
-
1
def redirect_path
-
8
if can_proceed_to_check_answers_page? || accepted_initial_allocation?
-
7
publish_provider_recruitment_cycle_allocation_get_edit_initial_request_path(
-
provider_code: allocation.accredited_body.provider_code,
-
recruitment_cycle_year: recruitment_cycle.year,
-
allocation_training_provider_code: allocation.provider.provider_code,
-
number_of_places: params[:number_of_places],
-
next_step: params[:next_step],
-
id: allocation.id,
-
request_type: params[:request_type],
-
)
-
else
-
1
publish_provider_recruitment_cycle_allocation_delete_initial_request_path(
-
provider_code: provider.provider_code,
-
recruitment_cycle_year: recruitment_cycle.year,
-
allocation_training_provider_code: training_provider.provider_code,
-
id: allocation.id,
-
)
-
end
-
end
-
-
1
private
-
-
1
def accepted_initial_allocation?
-
6
params[:next_step] == "number_of_places" && params[:request_type].present? && params[:request_type] == AllocationsView::RequestType::INITIAL
-
end
-
-
1
def number_of_places_validation_error?
-
11
params[:next_step] == "check_answers" && !form_object.valid?
-
end
-
-
1
def can_proceed_to_number_of_places_page?
-
17
(params[:next_step] == "number_of_places" && params[:request_type].present?) || number_of_places_validation_error?
-
end
-
-
1
def can_proceed_to_check_answers_page?
-
27
params[:next_step] == "check_answers" && params[:number_of_places].present? && form_object.valid?
-
end
-
-
1
def number_of_places_page?
-
(params[:step].present? && params[:step] == "number_of_places") || params[:number_of_places].present?
-
end
-
-
1
def check_answers_page?
-
params[:step].present? && params[:step] == "check_answers"
-
end
-
-
1
def training_provider
-
20
@training_provider ||= recruitment_cycle.providers.find_by(provider_code: params[:allocation_training_provider_code].upcase)
-
end
-
-
1
def allocation
-
41
@allocation ||= Allocation.includes(:provider, :accredited_body)
-
.find(params[:id])
-
end
-
-
1
def recruitment_cycle
-
67
cycle_year = params[:recruitment_cycle_year] || Settings.current_recruitment_cycle_year
-
-
67
@recruitment_cycle ||= RecruitmentCycle.find_by!(year: cycle_year)
-
end
-
-
1
def provider
-
20
@provider ||= recruitment_cycle.providers.find_by(provider_code: params[:provider_code])
-
end
-
-
1
def form_object
-
43
if params[:number_of_places]
-
24
number_of_places_form_object
-
else
-
19
request_form_object
-
end
-
end
-
-
1
def request_form_object
-
19
permitted_params = params
-
.slice(:request_type)
-
.permit(:request_type)
-
-
19
@request_form_object ||=
-
Publish::Allocations::EditInitial::RequestTypeForm.new(permitted_params)
-
end
-
-
1
def number_of_places_form_object
-
24
permitted_params = params
-
.slice(:number_of_places)
-
.permit(:number_of_places)
-
-
24
@number_of_places_form_object ||= Publish::Allocations::EditInitial::NumberOfPlacesForm.new(permitted_params)
-
end
-
end
-
1
class InitialRequestFlow
-
1
include Rails.application.routes.url_helpers
-
-
1
PE_SUBJECT_CODE = "C6".freeze
-
-
1
attr_reader :params
-
-
1
def initialize(params:)
-
35
@params = params
-
end
-
-
# rubocop:disable Lint/DuplicateBranch: Duplicate branch body detected
-
1
def template
-
30
if check_your_information_page?
-
3
"publish/providers/allocations/check_your_information"
-
27
elsif number_of_places_page? || number_of_places_zero?
-
11
"publish/providers/allocations/number_of_places"
-
16
elsif blank_search_query? || empty_search_results?
-
3
"publish/providers/allocations/initial_request"
-
13
elsif pick_a_provider_page?
-
2
"publish/providers/allocations/pick_a_provider"
-
else
-
11
"publish/providers/allocations/initial_request"
-
end
-
end
-
# rubocop:enable Lint/DuplicateBranch: Duplicate branch body detected
-
-
# rubocop:disable Lint/DuplicateBranch: Duplicate branch body detected
-
1
def locals
-
30
if number_of_places_page? || check_your_information_page?
-
{
-
14
training_provider: training_provider,
-
form_object: form_object,
-
}
-
16
elsif blank_search_query? || empty_search_results?
-
{
-
3
training_providers: training_providers_without_associated,
-
form_object: form_object,
-
provider: provider,
-
}
-
13
elsif pick_a_provider_page?
-
{
-
2
training_providers: training_providers_from_query,
-
}
-
else
-
{
-
11
training_providers: training_providers_without_associated,
-
form_object: form_object,
-
provider: provider,
-
}
-
end
-
end
-
# rubocop:enable Lint/DuplicateBranch: Duplicate branch body detected
-
-
1
def redirect?
-
11
return false if valid_number_of_places?
-
-
8
(training_provider_search? && one_search_result?) || training_provider_selected?
-
end
-
-
1
def allocation
-
5
if (training_provider_search? && one_search_result?) || training_provider_selected?
-
-
5
allocations.find_by(provider_code: training_provider[:provider_code])
-
end
-
end
-
-
1
def redirect_path
-
5
if allocation
-
edit_provider_recruitment_cycle_allocation_path(
-
provider.provider_code,
-
provider.recruitment_cycle_year,
-
training_provider.provider_code,
-
id: allocation[:id],
-
)
-
5
elsif (training_provider_search? && one_search_result?) || training_provider_selected?
-
5
initial_request_publish_provider_recruitment_cycle_allocations_path(
-
provider_code: provider.provider_code,
-
recruitment_cycle_year: recruitment_cycle.year,
-
training_provider_code: training_provider[:provider_code],
-
)
-
end
-
end
-
-
1
delegate :valid?, to: :form_object
-
-
1
private
-
-
1
def valid_number_of_places?
-
25
params[:number_of_places] && /\A\d+\z/.match?(params[:number_of_places]) && params[:number_of_places].to_i.positive?
-
end
-
-
1
def number_of_places_zero?
-
16
params[:number_of_places]&.to_i&.zero?
-
end
-
-
1
def training_provider_selected?
-
121
params[:training_provider_code].present? && params[:training_provider_code] != "-1"
-
end
-
-
1
def training_provider_search?
-
72
params[:training_provider_code] == "-1" && params[:training_provider_query].present? && params[:training_provider_query].size > 1
-
end
-
-
1
def one_search_result?
-
11
training_providers_from_query.count == 1
-
end
-
-
1
def form_object
-
47
permitted_params = params.slice(:training_provider_code, :training_provider_query, :number_of_places)
-
.permit(:training_provider_code, :training_provider_query, :number_of_places)
-
-
47
@form_object ||= Publish::InitialRequestForm.new(permitted_params)
-
end
-
-
1
def allocations
-
21
@allocations ||= Allocation.includes(:provider, :accredited_body)
-
.current_allocations.where(accredited_body_code: provider.provider_code)
-
end
-
-
1
def training_provider_search_service
-
30
API::V3::AccreditedProviderTrainingProvidersController::TrainingProviderSearch
-
end
-
-
1
def training_providers_with_fee_paying_pe_course
-
20
@training_providers_with_fee_paying_pe_course ||=
-
-
training_provider_search_service.new(
-
provider: provider,
-
params: {
-
filter: {
-
subjects: PE_SUBJECT_CODE, funding_type: "fee"
-
},
-
},
-
).call
-
end
-
-
1
def all_training_providers
-
14
@all_training_providers ||= training_provider_search_service.new(provider: provider, params: {}).call
-
end
-
-
1
def training_providers_with_previous_allocations
-
20
@training_providers_with_previous_allocations ||= allocations.map(&:provider)
-
end
-
-
1
def associated_training_providers
-
20
training_providers_with_previous_allocations + training_providers_with_fee_paying_pe_course
-
end
-
-
1
def training_providers_without_associated
-
14
return @training_providers_without_associated if @training_providers_without_associated
-
-
14
ids_to_reject = associated_training_providers.map(&:id)
-
-
14
@training_providers_without_associated = all_training_providers.reject do |provider|
-
14
ids_to_reject.include?(provider.id)
-
end
-
end
-
-
1
def training_providers_from_query
-
23
return @training_providers_from_query if @training_providers_from_query
-
-
4
query = params[:training_provider_query]
-
-
4
@training_providers_from_query ||= recruitment_cycle.providers
-
.provider_search(query)
-
.limit(5)
-
end
-
-
1
def training_providers_from_query_without_associated
-
6
ids_to_reject = associated_training_providers.map(&:id)
-
-
6
training_providers_from_query.reject do |r|
-
12
ids_to_reject.include?(r.id)
-
end
-
end
-
-
1
def recruitment_cycle
-
49
cycle_year = params[:recruitment_cycle_year] || Settings.current_recruitment_cycle_year
-
-
49
@recruitment_cycle ||= RecruitmentCycle.find_by!(year: cycle_year)
-
end
-
-
1
def provider
-
70
@provider ||= recruitment_cycle.providers.find_by(provider_code: params[:provider_code])
-
end
-
-
1
def empty_search_results?
-
30
return @empty_search_results if @empty_search_results
-
-
28
@empty_search_results = training_provider_search? && training_providers_from_query_without_associated.empty?
-
-
28
form_object.add_no_results_error if @empty_search_results
-
-
28
@empty_search_results
-
end
-
-
1
def pick_a_provider_page?
-
26
training_provider_search? && training_providers_from_query.count > 1
-
end
-
-
1
def number_of_places_page?
-
57
training_provider_selected? ||
-
32
(params[:training_provider_query].present? && params[:training_provider_query].size > 1 && one_search_result?) || params[:change]
-
end
-
-
1
def check_your_information_page?
-
46
training_provider_selected? && valid_number_of_places? && !params[:change]
-
end
-
-
1
def training_provider
-
24
@training_provider ||= if params[:training_provider_code] == "-1"
-
training_providers_from_query.first
-
else
-
19
recruitment_cycle.providers.find_by(provider_code: params[:training_provider_code].upcase)
-
end
-
end
-
-
1
def blank_search_query?
-
32
params[:training_provider_code] == "-1" && params[:training_provider_query].blank?
-
end
-
end
-
1
module Publish
-
1
class AboutYourOrganisationForm < BaseProviderForm
-
1
validates :train_with_us, presence: { message: "Enter details about training with you" }, if: :train_with_us_changed?
-
1
validates :train_with_disability, presence: { message: "Enter details about training with a disability" }, if: :train_with_disability_changed?
-
-
1
validates :train_with_us, words_count: { maximum: 250, message: "Reduce the word count for training with you" }
-
1
validates :train_with_disability, words_count: { maximum: 250, message: "Reduce the word count for training with disabilities and other needs" }
-
-
1
validate :add_enrichment_errors
-
-
1
FIELDS = %i[
-
train_with_us
-
train_with_disability
-
accrediting_provider_enrichments
-
].freeze
-
-
1
attr_accessor(*FIELDS)
-
-
1
def accredited_bodies
-
23
@accredited_bodies ||= provider.accredited_bodies.map do |ab|
-
7
accredited_body(ab)
-
end
-
end
-
-
1
private
-
-
1
def train_with_us_changed?
-
4
changed?(:train_with_us)
-
end
-
-
1
def train_with_disability_changed?
-
4
changed?(:train_with_disability)
-
end
-
-
1
def changed?(attribute)
-
8
public_send(attribute) != provider.public_send(attribute)
-
end
-
-
1
def accredited_body(provider_name:, provider_code:, description:)
-
7
AccreditedBody.new(
-
provider_name: provider_name,
-
provider_code: provider_code,
-
description: params_description(provider_code) || description,
-
)
-
end
-
-
1
def params_description(provider_code)
-
11
params[:accredited_bodies].to_h { |i| [i[:provider_code], i[:description]] }[provider_code] if params&.dig(:accredited_bodies).present?
-
end
-
-
1
def accrediting_provider_enrichments
-
7
accredited_bodies.map do |accredited_body|
-
{
-
7
UcasProviderCode: accredited_body.provider_code,
-
Description: accredited_body.description,
-
}
-
end
-
end
-
-
1
def compute_fields
-
7
provider.attributes.symbolize_keys.slice(*FIELDS).merge(new_attributes)
-
end
-
-
1
def new_attributes
-
7
params.except(:accredited_bodies).merge(accrediting_provider_enrichments: accrediting_provider_enrichments)
-
end
-
-
1
def add_enrichment_errors
-
4
accredited_bodies&.each_with_index do |accredited_body, _index|
-
4
if accredited_body.invalid?
-
errors.add :accredited_bodies, accredited_body.errors[:description].first
-
end
-
end
-
end
-
-
1
class AccreditedBody
-
1
include ActiveModel::Model
-
1
validates :description, words_count: { maximum: 100, message: lambda do |object, _data|
-
"Reduce the word count for #{object.provider_name}"
-
end }
-
-
1
attr_accessor :provider_name, :provider_code, :description
-
end
-
end
-
end
-
module Publish
-
class AccessRequestForm < BaseModelForm
-
alias_method :access_request, :model
-
-
validates :first_name, :last_name, :email_address,
-
:organisation, :reason,
-
presence: true
-
-
FIELDS = %i[
-
first_name
-
last_name
-
email_address
-
organisation
-
reason
-
].freeze
-
-
attr_accessor(*FIELDS, :user)
-
-
def initialize(user:, params: {})
-
@user = user
-
super(AccessRequest.new, params: params)
-
end
-
-
private
-
-
def requester_email
-
@requester_email ||= user.email
-
end
-
-
def assign_attributes_to_model
-
access_request.assign_attributes(fields.except(*fields_to_ignore_before_save).merge(requester_email: requester_email))
-
access_request.add_additional_attributes(requester_email)
-
end
-
-
def compute_fields
-
access_request.attributes.symbolize_keys.slice(*FIELDS).merge(new_attributes)
-
end
-
end
-
end
-
2
module Publish
-
2
class AgeRangeForm < BaseModelForm
-
2
alias_method :course, :model
-
-
2
include ::Courses::EditOptions::AgeRangeConcern
-
-
2
FIELDS = %i[
-
age_range_in_years
-
course_age_range_in_years_other_from
-
course_age_range_in_years_other_to
-
].freeze
-
-
2
attr_accessor(*FIELDS)
-
-
2
def course_age_range_in_years_other_from=(value)
-
10
@course_age_range_in_years_other_from = if value.present?
-
8
value.to_i
-
else
-
2
value
-
end
-
end
-
-
2
def course_age_range_in_years_other_to=(value)
-
10
@course_age_range_in_years_other_to = if value.present?
-
8
value.to_i
-
else
-
2
value
-
end
-
end
-
-
2
def initialize(*args)
-
15
super
-
-
15
if process_custom_range?
-
1
self.course_age_range_in_years_other_from = extract_from_years
-
1
self.course_age_range_in_years_other_to = extract_to_years
-
1
self.age_range_in_years = "other"
-
end
-
end
-
-
2
validates :age_range_in_years, presence: true
-
# Conditional validation to fix form validation
-
2
with_options if: :age_range_other? do
-
2
validates :course_age_range_in_years_other_from, numericality: {
-
only_integer: true,
-
allow_blank: true,
-
greater_than_or_equal_to: 0,
-
less_than_or_equal_to: 46,
-
}
-
2
validates :course_age_range_in_years_other_to, numericality: {
-
only_integer: true,
-
allow_blank: true,
-
greater_than_or_equal_to: 4,
-
less_than_or_equal_to: 50,
-
}
-
2
validate :age_range_from_and_to_missing
-
2
validate :age_range_from_and_to_reversed
-
2
validate :age_range_spans_at_least_4_years
-
end
-
-
2
private
-
-
2
def age_range_other?
-
50
age_range_in_years == "other"
-
end
-
-
2
def presets
-
6
course.age_range_options
-
end
-
-
2
def compute_fields
-
15
course.attributes.symbolize_keys.slice(*FIELDS).merge(new_attributes)
-
end
-
-
2
def process_custom_range?
-
15
age_range_in_years.present? && age_range_in_years != "other" && !presets.include?(age_range_in_years)
-
end
-
-
2
def extract_from_years
-
1
age_range_in_years.split("_").first
-
end
-
-
2
def extract_to_years
-
1
age_range_in_years.split("_").last
-
end
-
-
2
def age_range_from_and_to_missing
-
8
if age_range_in_years == "other"
-
8
if course_age_range_in_years_other_from.blank?
-
1
errors.add(:course_age_range_in_years_other_from, :blank)
-
end
-
-
8
if course_age_range_in_years_other_to.blank?
-
1
errors.add(:course_age_range_in_years_other_to, :blank)
-
end
-
end
-
end
-
-
2
def age_range_from_and_to_reversed
-
8
if age_range_in_years == "other" && course_age_range_in_years_other_from.present? && course_age_range_in_years_other_to.present? && (course_age_range_in_years_other_from.to_i > course_age_range_in_years_other_to.to_i)
-
2
errors.add(:course_age_range_in_years_other_from, :invalid)
-
end
-
end
-
-
2
def age_range_spans_at_least_4_years
-
8
if age_range_in_years == "other" && ((course_age_range_in_years_other_to.to_i - course_age_range_in_years_other_from.to_i).abs < 4)
-
3
errors.add(:course_age_range_in_years_other_to, :invalid)
-
end
-
end
-
end
-
end
-
2
module Publish
-
2
module Allocations
-
2
module EditInitial
-
2
class NumberOfPlacesForm
-
2
include ActiveModel::Model
-
-
2
attr_accessor :number_of_places
-
-
2
validates :number_of_places,
-
presence: { message: "You must enter a number" },
-
numericality: { message: "You must enter a number", other_than: 0, only_integer: true }
-
end
-
end
-
end
-
end
-
1
module Publish
-
1
module Allocations
-
1
module EditInitial
-
1
class RequestTypeForm
-
1
include ActiveModel::Model
-
-
1
attr_accessor :request_type
-
-
1
validates :request_type, presence: { message: "Select one option" }
-
end
-
end
-
end
-
end
-
1
module Publish
-
1
module Authentication
-
1
class MagicLinkForm
-
1
include ActiveModel::Model
-
-
1
attr_accessor :email
-
-
1
validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }
-
-
1
def submit
-
2
if valid?
-
1
GenerateAndSendMagicLinkService.call(user: user) if user.present?
-
1
true
-
else
-
1
false
-
end
-
end
-
-
1
private
-
-
1
def user
-
2
@user ||= User.find_by(email: email)
-
end
-
end
-
end
-
end
-
4
module Publish
-
4
class BaseCourseForm < BaseModelForm
-
4
alias_method :course, :model
-
-
4
def save!
-
26
if valid?
-
22
save_action
-
else
-
4
false
-
end
-
end
-
-
4
private
-
-
4
def after_successful_save_action
-
1
NotificationService::CourseUpdated.call(course: course)
-
end
-
-
4
def save_action
-
14
assign_attributes_to_model
-
14
if model.save!
-
14
after_successful_save_action
-
14
true
-
else
-
false
-
end
-
end
-
end
-
end
-
4
module Publish
-
4
class BaseModelForm
-
4
include ActiveModel::Model
-
4
include ActiveModel::AttributeAssignment
-
4
include ActiveModel::Validations::Callbacks
-
-
4
attr_accessor :model, :params, :fields
-
-
4
delegate :id, :persisted?, to: :model
-
-
4
def initialize(model, params: {})
-
157
@model = model
-
157
@params = params
-
157
@fields = compute_fields
-
157
assign_attributes(fields)
-
end
-
-
4
def save!
-
21
if valid?
-
14
assign_attributes_to_model
-
14
model.save!
-
else
-
7
false
-
end
-
end
-
-
4
private
-
-
4
def assign_attributes_to_model
-
18
model.assign_attributes(fields.except(*fields_to_ignore_before_save))
-
end
-
-
4
def compute_fields
-
raise(NotImplementedError)
-
end
-
-
4
def fields_to_ignore_before_save
-
16
[]
-
end
-
-
4
def new_attributes
-
143
params
-
end
-
end
-
end
-
4
module Publish
-
4
class BaseProviderForm < BaseModelForm
-
4
alias_method :provider, :model
-
end
-
end
-
1
module Publish
-
1
class CourseDeletionForm < BaseModelForm
-
1
alias_method :course, :model
-
-
1
FIELDS = %i[
-
confirm_course_code
-
].freeze
-
-
1
attr_accessor(*FIELDS)
-
-
1
validate :course_code_is_correct
-
-
1
def destroy!
-
2
if valid?
-
1
course.discard!
-
else
-
1
false
-
end
-
end
-
-
1
private
-
-
1
def compute_fields
-
4
course.attributes.symbolize_keys.slice(*FIELDS).merge(new_attributes)
-
end
-
-
1
def course_code_is_correct
-
2
return if course.course_code == confirm_course_code
-
-
1
errors.add(:confirm_course_code, :invalid_code, course_code: course.course_code)
-
end
-
end
-
end
-
1
module Publish
-
1
class CourseFeeForm < BaseModelForm
-
1
alias_method :course_enrichment, :model
-
-
1
include FundingTypeFormMethods
-
-
1
FIELDS = %i[
-
course_length
-
course_length_other_length
-
fee_uk_eu
-
fee_international
-
fee_details
-
financial_support
-
].freeze
-
-
1
attr_accessor(*FIELDS)
-
-
1
validates :course_length, presence: true
-
1
validates :fee_uk_eu, presence: true
-
-
1
validates :fee_uk_eu,
-
numericality: { allow_blank: true,
-
only_integer: true,
-
greater_than_or_equal_to: 0,
-
less_than_or_equal_to: 100000 }
-
-
1
validates :fee_international,
-
numericality: { allow_blank: true,
-
only_integer: true,
-
greater_than_or_equal_to: 0,
-
less_than_or_equal_to: 100000 }
-
-
1
validates :fee_details, words_count: { maximum: 250, message: :too_long }
-
1
validates :financial_support, words_count: { maximum: 250, message: :too_long }
-
-
1
private
-
-
1
def declared_fields
-
10
FIELDS
-
end
-
end
-
end
-
3
module Publish
-
3
class CourseInformationForm < BaseProviderForm
-
3
alias_method :course_enrichment, :model
-
-
3
FIELDS = %i[
-
about_course
-
interview_process
-
how_school_placements_work
-
].freeze
-
-
3
attr_accessor(*FIELDS)
-
-
3
delegate :recruitment_cycle_year, :provider_code, :name, to: :course
-
-
3
validates :about_course, presence: true
-
3
validates :about_course, words_count: { maximum: 400, message: :too_long }
-
-
3
validates :interview_process, words_count: { maximum: 250, message: :too_long }
-
-
3
validates :how_school_placements_work, presence: true
-
3
validates :how_school_placements_work, words_count: { maximum: 350, message: :too_long }
-
-
3
def save!
-
3
if valid?
-
2
assign_attributes_to_model
-
2
course_enrichment.status = :draft if course_enrichment.rolled_over?
-
2
course_enrichment.save!
-
else
-
1
false
-
end
-
end
-
-
3
private
-
-
3
def compute_fields
-
17
course_enrichment.attributes.symbolize_keys.slice(*FIELDS).merge(new_attributes)
-
end
-
end
-
end
-
1
module Publish
-
1
class CourseLocationForm < BaseCourseForm
-
1
FIELDS = %i[site_ids].freeze
-
-
1
attr_accessor(*FIELDS)
-
-
1
validate :no_locations_selected
-
-
1
def initialize(model, params: {})
-
7
@previous_site_names = model.sites.map(&:location_name)
-
7
super
-
end
-
-
1
def save_action
-
3
model.transaction do
-
3
assign_attributes_to_model
-
3
update_site_statuses
-
3
after_successful_save_action
-
3
true
-
end
-
end
-
-
1
private
-
-
1
attr_reader :previous_site_names
-
-
1
def after_successful_save_action
-
3
return if previous_site_names == updated_site_names
-
-
2
NotificationService::CourseSitesUpdated.call(
-
course: course,
-
previous_site_names: previous_site_names,
-
updated_site_names: updated_site_names,
-
)
-
end
-
-
1
def updated_site_names
-
5
@updated_site_names ||= course.sites.map(&:location_name)
-
end
-
-
1
def update_site_statuses
-
3
model.site_statuses.each do |site_status|
-
4
site_status.update!(
-
publish: :published,
-
status: :running,
-
)
-
end
-
end
-
-
1
def compute_fields
-
7
{ site_ids: course.site_ids }.merge(new_attributes)
-
end
-
-
1
def no_locations_selected
-
5
return if params[:site_ids].present?
-
-
2
errors.add(:site_ids, :no_locations)
-
end
-
end
-
end
-
1
module Publish
-
1
class CourseRequirementForm < BaseModelForm
-
1
alias_method :course_enrichment, :model
-
-
1
FIELDS = %i[
-
personal_qualities
-
other_requirements
-
].freeze
-
-
1
attr_accessor(*FIELDS)
-
-
1
validates :personal_qualities, words_count: { maximum: 100, message: :too_long }
-
1
validates :other_requirements, words_count: { maximum: 100, message: :too_long }
-
-
1
private
-
-
1
def compute_fields
-
10
course_enrichment.attributes.symbolize_keys.slice(*FIELDS).merge(new_attributes)
-
end
-
end
-
end
-
1
module Publish
-
1
class CourseSalaryForm < BaseModelForm
-
1
alias_method :course_enrichment, :model
-
-
1
include FundingTypeFormMethods
-
-
1
FIELDS = %i[
-
course_length
-
course_length_other_length
-
salary_details
-
].freeze
-
-
1
attr_accessor(*FIELDS)
-
-
1
validates :course_length, presence: true
-
1
validates :salary_details, presence: true
-
1
validates :salary_details, words_count: { maximum: 250, message: :too_long }
-
-
1
private
-
-
1
def declared_fields
-
6
FIELDS
-
end
-
end
-
end
-
1
module Publish
-
1
class CourseStudyModeForm < BaseModelForm
-
1
alias_method :course, :model
-
-
1
FIELDS = %i[study_mode].freeze
-
-
1
attr_accessor(*FIELDS)
-
-
1
validates :study_mode,
-
presence: true,
-
inclusion: { in: Course.study_modes.keys }
-
-
1
private
-
-
1
def compute_fields
-
4
course.attributes.symbolize_keys.slice(*FIELDS).merge(new_attributes)
-
end
-
end
-
end
-
2
module Publish
-
2
class CourseSubjectsForm < BaseCourseForm
-
2
alias_method :subject_ids, :params
-
-
2
def initialize(model, params: {})
-
6
@previous_subject_names = model.subjects.map(&:subject_name)
-
6
@previous_course_name = model.name
-
6
super
-
end
-
-
2
private
-
-
2
attr_reader :previous_subject_names, :previous_course_name
-
-
2
def valid?
-
6
super && assign_subjects_service.valid?
-
end
-
-
2
def compute_fields
-
6
{}
-
end
-
-
2
def after_successful_save_action
-
5
return if (previous_course_name == course.name) && (previous_subject_names == course.subjects.map(&:subject_name))
-
-
4
NotificationService::CourseSubjectsUpdated.call(
-
course: course,
-
previous_subject_names: previous_subject_names,
-
previous_course_name: previous_course_name,
-
)
-
end
-
-
2
def assign_subjects_service
-
11
@assign_subjects_service ||= ::Courses::AssignSubjectsService.call(course: course, subject_ids: subject_ids)
-
end
-
-
2
def save_action
-
5
if assign_subjects_service.save
-
5
after_successful_save_action
-
5
true
-
else
-
false
-
end
-
end
-
end
-
end
-
1
module Publish
-
1
class CourseVacanciesForm < BaseCourseForm
-
1
FIELDS = %i[
-
site_statuses
-
change_vacancies_confirmation
-
has_vacancies
-
].freeze
-
-
1
attr_accessor(*FIELDS)
-
-
1
delegate :recruitment_cycle_year, :provider_code, :running_site_statuses, :name, to: :course
-
-
1
validate :vacancies_confirmation
-
-
1
def initialize(*params)
-
32
@site_statuses = []
-
32
super
-
end
-
-
1
def site_statuses_attributes=(attributes)
-
7
@site_statuses = attributes.values
-
end
-
-
1
private
-
-
1
def after_successful_save_action
-
13
NotificationService::CourseVacanciesUpdated.call(
-
course: course,
-
vacancy_statuses: vacancy_statuses,
-
)
-
end
-
-
1
def vacancy_statuses
-
13
if course.has_multiple_running_sites_or_study_modes?
-
7
statuses_attributes.values.map do |value|
-
{
-
12
id: value[:id],
-
status: value[:vac_status],
-
}
-
end
-
else
-
[
-
{
-
6
id: statuses_attributes[:id],
-
status: statuses_attributes[:vac_status],
-
},
-
]
-
end
-
end
-
-
1
def compute_fields
-
32
course
-
.attributes
-
.symbolize_keys
-
.slice(*FIELDS)
-
.merge(new_attributes)
-
.merge(has_vacancies: course.has_vacancies?)
-
end
-
-
1
def assign_attributes_to_model
-
13
model.assign_attributes(model_attributes)
-
end
-
-
1
def model_attributes
-
13
fields
-
.symbolize_keys
-
.except(*fields_to_ignore_before_save)
-
.deep_merge(attributes_for_site_statuses)
-
end
-
-
1
def fields_to_ignore_before_save
-
13
FIELDS
-
end
-
-
1
def attributes_for_site_statuses
-
13
{ site_statuses_attributes: statuses_attributes }
-
end
-
-
1
def vacancies_confirmation
-
17
return if change_vacancies_confirmation.present? || params[:site_statuses_attributes].present?
-
-
4
errors.add(:change_vacancies_confirmation, vacancies_confirmation_error_message)
-
end
-
-
1
def vacancies_confirmation_error_message
-
4
course.has_vacancies? ? :confirm_close_application : :confirm_reopen_application
-
end
-
-
1
def statuses_attributes
-
32
if course.has_multiple_running_sites_or_study_modes?
-
14
attributes_for_multiple_site_statuses
-
else
-
18
attributes_for_single_site_status
-
end
-
end
-
-
1
def attributes_for_single_site_status
-
18
running_site_statuses.first.attributes.merge(vac_status: single_site_status_vacancy).with_indifferent_access
-
end
-
-
1
def attributes_for_multiple_site_statuses
-
14
params[:site_statuses_attributes].transform_values do |site_status_params|
-
24
site_status_params.except(:full_time, :part_time).merge(vac_status: site_status_vacancy(site_status_params))
-
end
-
end
-
-
1
def single_site_status_vacancy
-
18
case params[:change_vacancies_confirmation]
-
-
when "no_vacancies_confirmation"
-
6
"no_vacancies"
-
when "has_vacancies_confirmation"
-
12
course.full_time? ? "full_time_vacancies" : "part_time_vacancies"
-
end
-
end
-
-
1
def site_status_vacancy(site_status_params)
-
24
return "no_vacancies" if params[:has_vacancies] == "false"
-
-
20
VacancyStatusDeterminationService.call(
-
vacancy_status_full_time: site_status_params[:full_time],
-
vacancy_status_part_time: site_status_params[:part_time],
-
course: course,
-
)
-
end
-
end
-
end
-
2
module Publish
-
2
class CourseWithdrawalForm < BaseCourseForm
-
2
alias_method :course, :model
-
-
2
FIELDS = %i[
-
confirm_course_code
-
].freeze
-
-
2
attr_accessor(*FIELDS)
-
-
2
validate :course_code_is_correct
-
-
2
def save!
-
4
if valid?
-
3
course.withdraw
-
3
after_successful_save_action
-
3
true
-
else
-
1
false
-
end
-
end
-
-
2
private
-
-
2
def after_successful_save_action
-
3
NotificationService::CourseWithdrawn.call(course: course)
-
end
-
-
2
def compute_fields
-
7
course.attributes.symbolize_keys.slice(*FIELDS).merge(new_attributes)
-
end
-
-
2
def course_code_is_correct
-
5
return if course.course_code == confirm_course_code
-
-
2
errors.add(:confirm_course_code, :invalid_code, course_code: course.course_code)
-
end
-
end
-
end
-
1
module Publish
-
1
class DegreeGradeForm
-
# TODO: Refactor to use our form object pattern
-
1
include ActiveModel::Model
-
-
1
attr_accessor :grade
-
-
1
validates :grade, presence: { message: "Select the minimum degree classification you require" }
-
-
1
def save(course)
-
3
return false unless valid?
-
-
2
course.update(degree_grade: grade)
-
end
-
-
1
def self.build_from_course(course)
-
4
new(grade: course.degree_grade)
-
end
-
end
-
end
-
1
module Publish
-
1
class DegreeStartForm
-
# TODO: Refactor to use our form object pattern
-
1
include ActiveModel::Model
-
1
include ActiveModel::Validations::Callbacks
-
-
1
attr_accessor :degree_grade_required
-
-
1
before_validation :cast_degree_grade_required
-
-
1
validates :degree_grade_required, inclusion: { in: [true, false], message: "Select if you require a minimum degree classification" }
-
-
1
def save(course)
-
6
return false unless valid? && degree_grade_required_is_false?
-
-
2
course.update(degree_grade: "not_required")
-
end
-
-
1
def build_from_course(course)
-
6
self.degree_grade_required = handle_degree_grade_required(course)
-
end
-
-
1
private
-
-
1
def cast_degree_grade_required
-
6
self.degree_grade_required = ActiveModel::Type::Boolean.new.cast(degree_grade_required)
-
end
-
-
1
def degree_grade_required_is_false?
-
5
degree_grade_required == false
-
end
-
-
1
def handle_degree_grade_required(course)
-
6
if course.degree_grade == "not_required"
-
false
-
6
elsif course.degree_grade.present?
-
5
true
-
end
-
end
-
end
-
end
-
2
module Publish
-
2
module FundingTypeFormMethods
-
2
def other_course_length?
-
2
course_length_is_other?(course_length)
-
end
-
-
2
private
-
-
2
def compute_fields
-
16
course_enrichment
-
.attributes
-
.symbolize_keys
-
.slice(*declared_fields)
-
.merge(new_attributes)
-
.merge(**hydrate_other_course_length)
-
.symbolize_keys
-
end
-
-
2
def hydrate_other_course_length
-
16
return {} unless course_length_is_other?(course_enrichment[:course_length])
-
-
{
-
6
course_length: "Other",
-
course_length_other_length: course_enrichment[:course_length],
-
}
-
end
-
-
2
def fields_to_ignore_before_save
-
2
[:course_length_other_length]
-
end
-
-
2
def course
-
course_enrichment.course
-
end
-
-
2
def course_length_is_other?(value)
-
18
value.presence && %w[OneYear TwoYears].exclude?(value)
-
end
-
end
-
end
-
3
module Publish
-
3
class GcseRequirementsForm
-
3
include ActiveModel::Model
-
3
include ActiveModel::Validations::Callbacks
-
-
3
attr_accessor :accept_pending_gcse, :accept_gcse_equivalency, :accept_english_gcse_equivalency,
-
:accept_maths_gcse_equivalency, :accept_science_gcse_equivalency, :additional_gcse_equivalencies, :level
-
-
3
validates :accept_pending_gcse, inclusion: { in: [true, false], message: "Select if you consider candidates with pending GCSEs" }
-
3
validates :accept_gcse_equivalency, inclusion: { in: [true, false], message: "Select if you consider candidates with pending equivalency tests" }
-
15
validate :primary_or_secondary_equivalency_details_not_given, if: -> { equivalencies_not_selected? }
-
15
validates :additional_gcse_equivalencies, presence: { message: "Enter details about equivalency tests" }, if: -> { equivalencies_not_selected? }
-
3
validates :additional_gcse_equivalencies, words_count: { maximum: 200 }
-
-
3
def save(course)
-
6
return false unless valid?
-
-
3
set_equivalency_values_to_false unless accept_gcse_equivalency
-
-
3
course.update(
-
accept_pending_gcse: accept_pending_gcse,
-
accept_gcse_equivalency: accept_gcse_equivalency,
-
accept_english_gcse_equivalency: accept_english_gcse_equivalency,
-
accept_maths_gcse_equivalency: accept_maths_gcse_equivalency,
-
accept_science_gcse_equivalency: accept_science_gcse_equivalency,
-
additional_gcse_equivalencies: additional_gcse_equivalencies,
-
)
-
end
-
-
3
def self.build_from_course(course)
-
8
new(
-
accept_pending_gcse: course.accept_pending_gcse,
-
accept_gcse_equivalency: course.accept_gcse_equivalency,
-
accept_english_gcse_equivalency: course.accept_english_gcse_equivalency,
-
accept_maths_gcse_equivalency: course.accept_maths_gcse_equivalency,
-
accept_science_gcse_equivalency: course.accept_science_gcse_equivalency,
-
additional_gcse_equivalencies: course.additional_gcse_equivalencies,
-
level: course.level,
-
)
-
end
-
-
3
private
-
-
3
def primary_or_secondary_equivalency_details_not_given
-
4
if level == "primary"
-
1
errors.add(:equivalencies, "Select if you accept equivalency tests in English, maths or science")
-
else
-
3
errors.add(:equivalencies, "Select if you accept equivalency tests in English or maths")
-
end
-
end
-
-
3
def equivalencies_not_selected?
-
24
accept_gcse_equivalency.present? && accept_english_gcse_equivalency.blank? && accept_maths_gcse_equivalency.blank? && accept_science_gcse_equivalency.blank?
-
end
-
-
3
def set_equivalency_values_to_false
-
1
self.accept_english_gcse_equivalency = false
-
1
self.accept_maths_gcse_equivalency = false
-
1
self.accept_science_gcse_equivalency = false
-
1
self.additional_gcse_equivalencies = nil
-
end
-
end
-
end
-
1
module Publish
-
1
class InitialRequestForm
-
1
include ActiveModel::Model
-
-
1
attr_accessor :training_provider_code, :training_provider_query, :number_of_places
-
-
1
validates :training_provider_code, presence: { message: "Select or search for an organisation" }
-
1
validates :training_provider_query, presence: { message: "You need to add some information", if: :provider_search? }
-
1
validates :training_provider_query, length: { minimum: 2, message: "Please enter a minimum of two characters", if: :provider_search? }
-
1
validate :selected_number_of_places
-
-
1
def add_no_results_error
-
2
errors.add(
-
:training_provider_query,
-
"We could not find this organisation - please check your information and try again.",
-
)
-
end
-
-
1
private
-
-
1
def provider_search?
-
52
training_provider_code == "-1"
-
end
-
-
1
def selected_number_of_places
-
26
return if number_of_places.nil?
-
-
12
errors.add(:number_of_places, "You must enter a number") unless number_of_places_valid?
-
end
-
-
1
def number_of_places_valid?
-
12
!number_of_places.empty? &&
-
/\A\d+\z/.match?(number_of_places) &&
-
number_of_places.to_i.positive?
-
end
-
end
-
end
-
2
module Publish
-
2
module Interruption
-
2
class AcceptTermsForm < BaseModelForm
-
2
alias_method :user, :model
-
-
2
FIELDS = %i[
-
terms_accepted
-
].freeze
-
-
2
attr_accessor(*FIELDS)
-
-
2
validates :terms_accepted, acceptance: true
-
-
2
private
-
-
2
def compute_fields
-
12
{ terms_accepted: user.accepted_terms? }.merge(new_attributes).symbolize_keys
-
end
-
-
2
def assign_attributes_to_model
-
2
model.assign_attributes(accept_terms_date_utc: Time.zone.now)
-
end
-
end
-
end
-
end
-
1
module Publish
-
1
class LocationForm < BaseModelForm
-
1
FIELDS = %i[
-
location_name
-
urn
-
address1
-
address2
-
address3
-
address4
-
postcode
-
].freeze
-
-
1
attr_accessor(*FIELDS)
-
1
delegate :provider, to: :site
-
1
delegate :provider_code, :recruitment_cycle_year, to: :provider
-
-
1
def site
-
18
@model
-
end
-
-
1
validate :location_name_unique_to_provider
-
1
validates :location_name, presence: { message: "Enter a name" }
-
1
validates :address1, presence: { message: "Enter a building and street" }
-
1
validates :postcode, presence: { message: "Enter a postcode" }
-
1
validates :postcode, postcode: { message: "Postcode is not valid (for example, BN1 1AA)" }
-
1
validates :urn, reference_number_format: { allow_blank: true, minimum: 5, maximum: 6, message: "URN must be 5 or 6 numbers" }
-
-
1
private
-
-
1
def assign_attributes_to_site
-
site.assign_attributes(fields.except(*fields_to_ignore_before_save))
-
end
-
-
1
def compute_fields
-
4
site.attributes.symbolize_keys.slice(*FIELDS).merge(new_attributes)
-
end
-
-
1
def location_name_unique_to_provider
-
2
sibling_sites = provider.sites - [site]
-
-
2
if location_name.in?(sibling_sites.pluck(:location_name))
-
errors.add(:location_name, "Name is in use by another location")
-
end
-
end
-
end
-
end
-
1
module Publish
-
1
class NotificationForm < BaseModelForm
-
1
alias_method :user, :model
-
-
1
FIELDS = %i[
-
explicitly_enabled
-
].freeze
-
-
1
attr_accessor(*FIELDS)
-
-
1
validates :explicitly_enabled, inclusion: { in: [true, false], message: "Please select one option" }
-
-
1
def save!
-
2
if valid?
-
1
user_notification_preferences.update(enable_notifications: explicitly_enabled)
-
else
-
1
false
-
end
-
end
-
-
1
private
-
-
1
def compute_fields
-
4
{ explicitly_enabled: preference_selected? }.merge(new_attributes).symbolize_keys
-
end
-
-
1
def user_notification_preferences
-
5
@user_notification_preferences ||= UserNotificationPreferences.new(user_id: user.id)
-
end
-
-
1
def preference_selected?
-
4
return if user_notification_preferences.updated_at.blank?
-
-
user_notification_preferences.enabled?
-
end
-
end
-
end
-
1
module Publish
-
1
class ProviderContactForm < BaseProviderForm
-
1
FIELDS = %i[
-
email telephone urn website ukprn
-
address1 address2 address3 address4 postcode region_code
-
].freeze
-
-
1
attr_accessor(*FIELDS)
-
-
1
delegate :recruitment_cycle_year, :provider_code, :provider_name, :lead_school?, to: :provider
-
-
1
validates :email, email_address: { message: "Enter an email address in the correct format, like name@example.com" }
-
1
validates :telephone, phone: { message: "Enter a valid telephone number" }
-
1
validates :urn, reference_number_format: { allow_blank: false, minimum: 5, maximum: 6, message: "URN must be 5 or 6 numbers" }, if: :lead_school?
-
1
validates :ukprn, reference_number_format: { allow_blank: false, minimum: 8, maximum: 8, message: "UKPRN must be 8 numbers" }
-
-
1
private
-
-
1
def compute_fields
-
4
provider.attributes.symbolize_keys.slice(*FIELDS).merge(new_attributes)
-
end
-
end
-
end
-
2
module Publish
-
2
class ProviderVisaForm < BaseProviderForm
-
2
FIELDS = %i[
-
can_sponsor_student_visa
-
can_sponsor_skilled_worker_visa
-
].freeze
-
-
2
attr_accessor(*FIELDS)
-
-
2
validates :can_sponsor_student_visa, inclusion: { in: [true, false], message: "Select if candidates can get a sponsored Student visa" }
-
2
validates :can_sponsor_skilled_worker_visa, inclusion: { in: [true, false], message: "Select if candidates can get a sponsored Skilled Worker visa" }
-
-
2
private
-
-
2
def compute_fields
-
7
provider.attributes.symbolize_keys.slice(*FIELDS).merge(new_attributes)
-
end
-
end
-
end
-
2
module Publish
-
2
class RepeatRequestForm
-
2
include ActiveModel::Model
-
-
2
attr_accessor :request_type
-
-
2
validates :request_type, presence: { message: "Select one option" }
-
end
-
end
-
1
module Publish
-
1
class SubjectRequirementForm
-
# TODO: Refactor to use our form object pattern
-
1
include ActiveModel::Model
-
1
include ActiveModel::Validations::Callbacks
-
-
1
attr_accessor :additional_degree_subject_requirements, :degree_subject_requirements
-
-
1
before_validation :cast_additional_degree_subject_requirements
-
-
1
validates :additional_degree_subject_requirements, inclusion: { in: [true, false], message: "Select if you have degree subject requirements" }
-
4
validates :degree_subject_requirements, presence: { message: "Enter details of degree subject requirements" }, if: -> { additional_degree_subject_requirements }
-
-
1
def save(course)
-
3
return false unless valid?
-
-
2
course.update(
-
additional_degree_subject_requirements: additional_degree_subject_requirements,
-
2
degree_subject_requirements: additional_degree_subject_requirements.present? ? degree_subject_requirements : nil,
-
)
-
end
-
-
1
def self.build_from_course(course)
-
8
new(
-
additional_degree_subject_requirements: course.additional_degree_subject_requirements,
-
degree_subject_requirements: course.degree_subject_requirements,
-
)
-
end
-
-
1
private
-
-
1
def cast_additional_degree_subject_requirements
-
3
self.additional_degree_subject_requirements = ActiveModel::Type::Boolean.new.cast(additional_degree_subject_requirements)
-
end
-
end
-
end
-
2
module Support
-
2
class EditCourseForm
-
2
include ActiveModel::Model
-
2
include ActiveModel::Validations
-
-
2
FIELDS = %i[
-
course_code name
-
].freeze
-
-
2
attr_accessor(*FIELDS)
-
2
attr_accessor :start_date_day, :start_date_month, :start_date_year, :course, :applications_open_from_day, :applications_open_from_month, :applications_open_from_year, :is_send
-
2
validate :validate_start_date_format
-
2
validate :validate_applications_open_from_format
-
-
2
def initialize(course)
-
26
@course = course
-
-
26
super(
-
course_code: @course.course_code,
-
name: @course.name,
-
start_date_day: @course.start_date&.day,
-
start_date_month: @course.start_date&.month,
-
start_date_year: @course.start_date&.year,
-
applications_open_from_day: @course.applications_open_from&.day,
-
applications_open_from_month: @course.applications_open_from&.month,
-
applications_open_from_year: @course.applications_open_from&.year,
-
is_send: @course.is_send,
-
)
-
end
-
-
2
def save
-
12
return false unless valid?
-
-
3
@course.save
-
end
-
-
2
def valid?
-
17
super()
-
17
assign_attributes_to_course
-
17
course.valid?
-
17
promote_errors_from_course
-
17
errors.none?
-
end
-
-
2
def start_date
-
70
@start_date ||= check_date(:start_date)
-
end
-
-
2
def applications_open_from
-
66
@applications_open_from ||= check_date(:applications_open_from)
-
end
-
-
2
private
-
-
2
def check_date(date_type)
-
46
date_args = date_array(date_type).map(&:to_i)
-
-
46
return Date.new(*date_args) if Date.valid_date?(*date_args)
-
-
14
date_struct(date_type)
-
end
-
-
2
def date_struct(date_type)
-
14
Struct.new(:day, :month, :year).new(
-
send("#{date_type}_day"),
-
send("#{date_type}_month"),
-
send("#{date_type}_year"),
-
)
-
end
-
-
2
def date_array(date_type)
-
80
[send("#{date_type}_year"), send("#{date_type}_month"), send("#{date_type}_day")]
-
end
-
-
2
def assign_attributes_to_course
-
attributes = {
-
17
course_code: course_code,
-
name: name,
-
start_date: start_date,
-
applications_open_from: applications_open_from,
-
is_send: send?,
-
}
-
-
17
course.assign_attributes(attributes)
-
end
-
-
2
def promote_errors_from_course
-
17
errors.merge!(course.errors)
-
end
-
-
2
def send?
-
17
ActiveModel::Type::Boolean.new.cast(is_send)
-
end
-
-
2
def validate_start_date_format
-
17
validate_date(:start_date)
-
end
-
-
2
def validate_applications_open_from_format
-
17
validate_date(:applications_open_from)
-
end
-
-
2
def validate_date(date_type)
-
34
return if date_args_blank?(date_type)
-
-
26
errors.add(date_type, "#{attribute(date_type)} format is invalid") unless valid_date?(date_type)
-
end
-
-
2
def attribute(date_type)
-
8
case date_type
-
when :applications_open_from
-
4
"#{date_type.to_s.humanize.capitalize} date"
-
else
-
4
date_type.to_s.humanize.capitalize
-
end
-
end
-
-
2
def valid_date?(date_type)
-
26
send(date_type).is_a?(Date)
-
end
-
-
2
def date_args_blank?(date_type)
-
34
date_array(date_type).any?(&:blank?)
-
end
-
end
-
end
-
4
module AgeRangeErrorsViewHelper
-
4
def expand_another_age_range?(errors)
-
9
(course.other_age_range? && course.age_range_in_years.present?) ||
-
9
(errors && (errors[:age_range_in_years_to].present? || errors[:age_range_in_years_from].present?))
-
end
-
-
4
def age_range_from_field_value
-
9
if course.other_age_range? && course.age_range_in_years.present?
-
course.age_range_in_years.split("_").first
-
else
-
9
""
-
end
-
end
-
-
4
def age_range_to_field_value
-
9
if course.other_age_range? && course.age_range_in_years.present?
-
course.age_range_in_years.split("_").last
-
else
-
9
""
-
end
-
end
-
end
-
4
module AllocationCycleHelper
-
4
def current_allocation_cycle_period_text
-
4
"#{Settings.allocation_cycle_year} to #{Settings.allocation_cycle_year + 1}"
-
end
-
-
4
def next_allocation_cycle_period_text
-
205
"#{Settings.allocation_cycle_year + 1} to #{Settings.allocation_cycle_year + 2}"
-
end
-
end
-
4
module ApplicationHelper
-
4
include Pagy::Frontend
-
-
4
def pagy_govuk_nav(pagy)
-
26
render "pagy/paginator", pagy: pagy
-
end
-
-
4
def header_items(current_user)
-
return unless current_user
-
-
items = [{ name: t("header.items.sign_out"), url: sign_out_path }]
-
items
-
end
-
-
# rubocop:disable Rails/HelperInstanceVariable
-
# TODO: refactor enrichment_error_link method to not use an instance variable
-
4
def enrichment_error_link(model, field, error)
-
2
href = case model
-
when :course
-
2
enrichment_error_url(
-
provider_code: @provider.provider_code,
-
course: @course,
-
field: field.to_s,
-
message: error,
-
)
-
when :provider
-
provider_enrichment_error_url(
-
provider: @provider,
-
field: field.to_s,
-
)
-
end
-
-
2
govuk_inset_text(classes: "app-inset-text--narrow-border app-inset-text--error") do
-
2
govuk_link_to(error, href)
-
end
-
end
-
-
# TODO: refactor enrichment_summary method to not use an instance variable
-
4
def enrichment_summary(summary_list, model, key, value, fields, truncate_value: true, action_path: nil, action_visually_hidden_text: nil)
-
280
action = render_action(action_path, action_visually_hidden_text)
-
-
734
if fields.select { |field| @errors&.key? field.to_sym }.any?
-
1
errors = fields.map { |field|
-
2
@errors[field.to_sym]&.map { |error| enrichment_error_link(model, field, error) }
-
}.flatten
-
-
1
value = raw(*errors)
-
1
action = nil
-
279
elsif truncate_value
-
225
classes = "app-summary-list__value--truncate"
-
end
-
-
280
if value.blank?
-
66
value = raw("<span class=\"app-!-colour-muted\">Empty</span>")
-
end
-
-
280
summary_list.row(html_attributes: { data: { qa: "enrichment__#{fields.first}" } }) do |row|
-
560
row.key { key.html_safe }
-
560
row.value(classes: classes) { value }
-
280
if action
-
261
row.action(action)
-
else
-
19
row.action
-
end
-
end
-
end
-
# rubocop:enable Rails/HelperInstanceVariable
-
-
4
def display_phase_banner_border?(user)
-
990
user && !user.admin? && user.providers.where(recruitment_cycle: RecruitmentCycle.current).one?
-
end
-
-
4
private
-
-
4
def render_action(action_path, action_visually_hidden_text)
-
280
return if action_path.blank?
-
-
{
-
261
href: action_path,
-
visually_hidden_text: action_visually_hidden_text,
-
}
-
end
-
end
-
4
module BreadcrumbHelper
-
4
def render_breadcrumbs(type)
-
105
breadcrumbs = send("#{type}_breadcrumb")
-
-
# Don't link last item in breadcrumb
-
105
breadcrumbs[breadcrumbs.keys.last] = nil
-
-
105
if breadcrumbs && !FeatureService.enabled?(:new_publish_navigation)
-
98
render GovukComponent::BreadcrumbsComponent.new(
-
breadcrumbs: breadcrumbs,
-
classes: "govuk-!-display-none-print",
-
)
-
end
-
end
-
-
# rubocop:disable Rails/HelperInstanceVariable
-
4
def organisations_breadcrumb
-
105
current_user.has_multiple_providers? ? { "Organisations" => root_path } : {}
-
end
-
-
4
def provider_breadcrumb
-
105
path = publish_provider_path(code: @provider.provider_code)
-
105
organisations_breadcrumb.merge({ @provider.provider_name => path })
-
end
-
-
4
def recruitment_cycle_breadcrumb
-
73
if @provider.rolled_over?
-
path = publish_provider_recruitment_cycle_path(@provider.provider_code, @recruitment_cycle.year)
-
provider_breadcrumb.merge({ @recruitment_cycle.title => path })
-
else
-
73
provider_breadcrumb
-
end
-
end
-
-
4
def courses_breadcrumb
-
62
path = publish_provider_recruitment_cycle_courses_path(@provider.provider_code)
-
62
recruitment_cycle_breadcrumb.merge({ "Courses" => path })
-
end
-
-
4
def course_breadcrumb
-
35
path = publish_provider_recruitment_cycle_course_path(@provider.provider_code, course.recruitment_cycle_year, course.course_code)
-
35
courses_breadcrumb.merge({ course.name_and_code => path })
-
end
-
-
4
def sites_breadcrumb
-
5
path = publish_provider_recruitment_cycle_locations_path(@provider.provider_code, @recruitment_cycle.year)
-
5
recruitment_cycle_breadcrumb.merge({ "Locations" => path })
-
end
-
-
4
def organisation_details_breadcrumb
-
6
path = details_publish_provider_recruitment_cycle_path(@provider.provider_code, @recruitment_cycle.year)
-
6
recruitment_cycle_breadcrumb.merge({ "About your organisation" => path })
-
end
-
-
4
def users_breadcrumb
-
path = details_publish_provider_recruitment_cycle_path(@provider.provider_code, @recruitment_cycle.year)
-
recruitment_cycle_breadcrumb.merge({ "Users" => path })
-
end
-
-
4
def edit_site_breadcrumb
-
1
path = edit_publish_provider_recruitment_cycle_location_path(@provider.provider_code, @recruitment_cycle.year, @site.id)
-
1
sites_breadcrumb.merge({ @site.location_name.dup => path })
-
end
-
-
4
def new_site_breadcrumb
-
1
path = new_publish_provider_recruitment_cycle_location_path(@provider.provider_code)
-
1
sites_breadcrumb.merge({ "Add a location" => path })
-
end
-
-
4
def training_providers_breadcrumb
-
3
path = publish_provider_recruitment_cycle_training_providers_path(@provider.provider_code, @provider.recruitment_cycle_year)
-
3
provider_breadcrumb.merge({ "Courses as an accredited body" => path })
-
end
-
-
4
def training_provider_courses_breadcrumb
-
1
path = publish_provider_recruitment_cycle_training_provider_courses_path(@provider.provider_code, @provider.recruitment_cycle_year, @training_provider.provider_code)
-
1
training_providers_breadcrumb.merge({ "#{@training_provider.provider_name}’s courses" => path })
-
end
-
-
4
def allocations_breadcrumb
-
25
path = publish_provider_recruitment_cycle_allocations_path(@provider.provider_code, @provider.recruitment_cycle_year)
-
25
provider_breadcrumb.merge({ "Request PE courses for #{next_allocation_cycle_period_text}" => path })
-
end
-
-
4
def allocations_closed_breadcrumb
-
4
path = publish_provider_recruitment_cycle_allocations_path(@provider.provider_code, @provider.recruitment_cycle_year)
-
4
provider_breadcrumb.merge({ "PE courses for #{next_allocation_cycle_period_text}" => path })
-
end
-
# rubocop:enable Rails/HelperInstanceVariable
-
end
-
# frozen_string_literal: true
-
-
4
module LayoutHelper
-
# Enables inheriting from and extending another layout.
-
# Adapted from the nestive gem: https://github.com/rwz/nestive/blob/master/lib/nestive/layout_helper.rb#L84
-
4
def extends_layout(layout, &block)
-
738
layout = layout.to_s
-
738
layout = "layouts/#{layout}" unless layout.include?("/")
-
738
view_flow.get(:layout).replace(capture(&block).to_s)
-
-
738
render(template: layout)
-
end
-
end
-
4
module LocationHelper
-
4
def urn_required?(recruitment_cycle_year)
-
8
recruitment_cycle_year >= Site::URN_2022_REQUIREMENTS_REQUIRED_FROM
-
end
-
end
-
4
module NavigationBarHelper
-
4
def render_navigation_bar?(provider)
-
43
provider && !current_page?(root_path) && provider.recruitment_cycle && !request.path.include?("support")
-
end
-
end
-
4
module ProviderHelper
-
4
def visa_sponsorship_status(provider)
-
6
if !provider.declared_visa_sponsorship?
-
visa_sponsorship_call_to_action(provider)
-
6
elsif provider.can_sponsor_student_visa || provider.can_sponsor_skilled_worker_visa
-
6
"#{visa_sponsorship_short_status(provider)} can be sponsored"
-
else
-
"Visas cannot be sponsored"
-
end
-
end
-
-
4
def visa_sponsorship_short_status(provider)
-
8
if !provider.declared_visa_sponsorship?
-
visa_sponsorship_call_to_action(provider)
-
8
elsif provider.can_sponsor_all_visas?
-
1
"Student and Skilled Worker visas"
-
7
elsif provider.can_only_sponsor_student_visa?
-
4
"Student visas"
-
3
elsif provider.can_only_sponsor_skilled_worker_visa?
-
3
"Skilled Worker visas"
-
end
-
end
-
-
# The method below can be deleted when the feature flag is removed
-
4
def rolled_over_and_new_nav?(provider)
-
38
provider.rolled_over? && FeatureService.enabled?(:new_publish_navigation)
-
end
-
-
4
private
-
-
4
def visa_sponsorship_call_to_action(provider)
-
govuk_inset_text(classes: "app-inset-text--narrow-border app-inset-text--important") do
-
raw("<p class=\"govuk-heading-s app-inset-text__title\">Can you sponsor visas?</p>") +
-
govuk_link_to(
-
"Select if visas can be sponsored",
-
visas_publish_provider_recruitment_cycle_path(
-
provider.provider_code,
-
provider.recruitment_cycle_year,
-
),
-
)
-
end
-
end
-
-
4
def google_form_url_for(settings, email, provider)
-
"#{settings.url}&#{{ settings.email_entry => email, settings.provider_code_entry => provider.provider_code }.to_query}"
-
end
-
-
4
def is_current_cycle(cycle_year)
-
Settings.current_recruitment_cycle_year == cycle_year.to_i
-
end
-
end
-
# frozen_string_literal: true
-
-
4
module PublishHelper
-
4
def markdown(source)
-
14
render = Govuk::MarkdownRenderer
-
# Options: https://github.com/vmg/redcarpet#and-its-like-really-simple-to-use
-
# lax_spacing: HTML blocks do not require to be surrounded by an empty line as in the Markdown standard.
-
# autolink: parse links even when they are not enclosed in <> characters
-
14
options = { autolink: true, lax_spacing: true }
-
14
markdown = Redcarpet::Markdown.new(render, options)
-
14
markdown.render(source).html_safe
-
-
# Convert quotes to smart quotes
-
14
source_with_smart_quotes = smart_quotes(source)
-
14
markdown.render(source_with_smart_quotes).html_safe
-
end
-
-
4
def smart_quotes(string)
-
30
return "" if string.blank?
-
-
30
RubyPants.new(string, [2, :dashes], ruby_pants_options).to_html
-
end
-
-
4
private
-
-
# Use characters rather than HTML entities for smart quotes this matches how
-
# we write smart quotes in templates and allows us to use them in <title>
-
# elements
-
# https://github.com/jmcnevin/rubypants/blob/master/lib/rubypants.rb
-
4
def ruby_pants_options
-
30
{
-
double_left_quote: "“",
-
double_right_quote: "”",
-
single_left_quote: "‘",
-
single_right_quote: "’",
-
ellipsis: "…",
-
em_dash: "—",
-
en_dash: "–",
-
}
-
end
-
end
-
# frozen_string_literal: true
-
-
4
module RecruitmentCycleHelper
-
4
def current_recruitment_cycle_period_text
-
1
"#{Settings.current_recruitment_cycle_year} to #{Settings.current_recruitment_cycle_year + 1}"
-
end
-
-
4
def next_recruitment_cycle_period_text
-
9
"#{Settings.current_recruitment_cycle_year + 1} to #{Settings.current_recruitment_cycle_year + 2}"
-
end
-
-
4
def previous_recruitment_cycle_period_text
-
"#{Settings.current_recruitment_cycle_year - 1} to #{Settings.current_recruitment_cycle_year}"
-
end
-
end
-
# frozen_string_literal: true
-
-
4
module Support
-
4
module TimeHelper
-
4
def gov_uk_format(time)
-
11
time.strftime("%-l:%M%P on %-e %B %Y")
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
4
module SupportEmailHelper
-
4
def support_email(name: nil, subject: nil, classes: nil)
-
222
govuk_mail_to(Settings.support_email, name, subject: subject, class: classes)
-
end
-
end
-
# frozen_string_literal: true
-
-
4
module UserHelper
-
4
def full_name(user)
-
[user.first_name, user.last_name].join(" ")
-
end
-
-
4
def user_details(user)
-
9
"#{user[:first_name]} #{user[:last_name]} <#{user[:email]}>"
-
end
-
end
-
# frozen_string_literal: true
-
-
4
module VacancyHelper
-
4
def vacancy_available_for_course_site_status?(course, site_status, vacancy_study_mode = nil)
-
40
case course.study_mode
-
when "full_time"
-
16
site_status.full_time_vacancies?
-
when "part_time"
-
2
site_status.part_time_vacancies?
-
when "full_time_or_part_time"
-
22
vacancy_available_for_study_mode?(site_status, vacancy_study_mode)
-
else
-
false
-
end
-
end
-
-
4
private
-
-
4
def vacancy_available_for_study_mode?(site_status, vacancy_study_mode)
-
22
return true if site_status.both_full_time_and_part_time_vacancies?
-
-
15
case vacancy_study_mode
-
when :part_time
-
6
site_status.part_time_vacancies?
-
when :full_time
-
6
site_status.full_time_vacancies?
-
else
-
3
false
-
end
-
end
-
end
-
4
module ViewHelper
-
4
def govuk_back_link_to(url = :back, body = "Back")
-
249
render GovukComponent::BackLinkComponent.new(
-
text: body,
-
href: url,
-
classes: "govuk-!-display-none-print",
-
html_attributes: {
-
data: {
-
qa: "page-back",
-
},
-
},
-
)
-
end
-
-
4
def search_ui_url(relative_path)
-
17
URI.join(Settings.search_ui.base_url, relative_path).to_s
-
end
-
-
4
def search_ui_course_page_url(provider_code:, course_code:)
-
17
search_ui_url("/course/#{provider_code}/#{course_code}")
-
end
-
-
4
def bat_contact_email_address
-
2481
Settings.support_email
-
end
-
-
4
def bat_contact_email_address_with_wrap
-
# https://developer.mozilla.org/en-US/docs/Web/HTML/Element/wbr
-
# The <wbr> element will not be copied when copying and pasting the email address
-
1238
bat_contact_email_address.gsub("@", "<wbr>@").html_safe
-
end
-
-
4
def bat_contact_mail_to(name = nil, **kwargs)
-
1243
govuk_mail_to bat_contact_email_address, name || bat_contact_email_address_with_wrap, **kwargs
-
end
-
-
# def feedback_link_to
-
# t("feedback.link")
-
# end
-
-
4
def title_with_error_prefix(title, error)
-
286
"#{t('page_titles.error_prefix') if error}#{title}"
-
end
-
-
4
def enrichment_error_url(provider_code:, course:, field:, message: nil)
-
5
base = "/publish/organisations/#{provider_code}/#{course.recruitment_cycle_year}/courses/#{course.course_code}"
-
-
5
if field.to_sym == :base
-
2
base_errors_hash(provider_code, course)[message]
-
else
-
{
-
3
about_course: "#{base}/about?display_errors=true#publish-course-information-form-about-course-field-error",
-
how_school_placements_work: "#{base}/about?display_errors=true#publish-course-information-form-how-school-placements-work-field-error",
-
fee_uk_eu: "#{base}/fees?display_errors=true#fee_uk_eu-error",
-
3
course_length: "#{base + (course.is_fee_based? ? '/fees' : '/salary')}?display_errors=true#course_length-error",
-
salary_details: "#{base}/salary?display_errors=true#salary_details-error",
-
required_qualifications: "#{base}/requirements?display_errors=true#required_qualifications_wrapper",
-
age_range_in_years: "#{base}/age-range?display_errors=true",
-
}.with_indifferent_access[field]
-
end
-
end
-
-
4
def provider_enrichment_error_url(provider:, field:)
-
1
base = "/publish/organisations/#{provider.provider_code}/#{provider.recruitment_cycle_year}"
-
-
{
-
1
"train_with_us" => "#{base}/about?display_errors=true#provider_train_with_us",
-
"train_with_disability" => "#{base}/about?display_errors=true#provider_train_with_disability",
-
"email" => "#{base}/contact?display_errors=true#provider_email",
-
"website" => "#{base}/contact?display_errors=true#provider_website",
-
"telephone" => "#{base}/contact?display_errors=true#provider_telephone",
-
"address1" => "#{base}/contact?display_errors=true#provider_address1",
-
"address3" => "#{base}/contact?display_errors=true#provider_address3",
-
"address4" => "#{base}/contact?display_errors=true#provider_address4",
-
"postcode" => "#{base}/contact?display_errors=true#provider_postcode",
-
}[field]
-
end
-
-
# def environment_colour
-
# return "purple" if sandbox_mode?
-
-
# {
-
# "development" => "grey",
-
# "qa" => "orange",
-
# "review" => "purple",
-
# "rollover" => "turquoise",
-
# "staging" => "red",
-
# "unknown-environment" => "yellow",
-
# }[Settings.environment.selector_name]
-
# end
-
-
# def environment_label
-
# Settings.environment.label
-
# end
-
-
# def environment_header_class
-
# "app-header--#{Settings.environment.selector_name}"
-
# end
-
-
# def sandbox_mode?
-
# Settings.environment.selector_name == "sandbox"
-
# end
-
-
## Ad-hoc, informally specified, and bug-ridden Ruby implementation of half
-
## of https://github.com/JedWatson/classnames.
-
##
-
## Example usage:
-
## <input class="<%= cns("govuk-input", "govuk-input--width-10": is_small) %>">
-
4
def classnames(*args)
-
22
args.reduce("") do |str, arg|
-
classes =
-
-
44
case arg
-
when Hash
-
44
arg.reduce([]) { |cs, (classname, condition)| cs + [condition ? classname : nil] }
-
when String
-
22
[arg]
-
else
-
[]
-
end
-
44
([str] + classes).compact_blank.join(" ")
-
end
-
end
-
-
4
alias_method :cns, :classnames
-
-
4
private
-
-
4
def base_errors_hash(provider_code, course)
-
{
-
2
"Select if visas can be sponsored" =>
-
visas_publish_provider_recruitment_cycle_path(provider_code, course.recruitment_cycle_year),
-
"You must provide a Unique Reference Number (URN) for all course locations" =>
-
locations_publish_provider_recruitment_cycle_course_path(provider_code, course.recruitment_cycle_year, course.course_code),
-
"Enter a Unique Reference Number (URN) for all course locations" =>
-
locations_publish_provider_recruitment_cycle_course_path(provider_code, course.recruitment_cycle_year, course.course_code),
-
"You must provide a UK provider reference number (UKPRN)" =>
-
contact_publish_provider_recruitment_cycle_path(provider_code, course.recruitment_cycle_year),
-
"You must provide a UK provider reference number (UKPRN) and URN" =>
-
contact_publish_provider_recruitment_cycle_path(provider_code, course.recruitment_cycle_year),
-
"Enter a UK Provider Reference Number (UKPRN)" =>
-
contact_publish_provider_recruitment_cycle_path(provider_code, course.recruitment_cycle_year),
-
"Enter a UK Provider Reference Number (UKPRN) and URN" =>
-
contact_publish_provider_recruitment_cycle_path(provider_code, course.recruitment_cycle_year),
-
"Enter degree requirements" =>
-
degrees_start_publish_provider_recruitment_cycle_course_path(provider_code, course.recruitment_cycle_year, course.course_code, display_errors: true),
-
"Enter GCSE requirements" =>
-
gcses_pending_or_equivalency_tests_publish_provider_recruitment_cycle_course_path(provider_code, course.recruitment_cycle_year, course.course_code, display_errors: true),
-
}
-
end
-
end
-
4
class ApplicationJob < ActiveJob::Base
-
4
retry_on ActiveRecord::Deadlocked
-
-
4
discard_on ActiveJob::DeserializationError
-
end
-
4
class GeocodeJob < ApplicationJob
-
4
queue_as :geocoding
-
-
4
def perform(klass, id)
-
1
RequestStore.store[:job_id] = provider_job_id
-
1
RequestStore.store[:job_queue] = queue_name
-
-
1
record = klass.classify.safe_constantize.find(id)
-
1
GeocoderService.geocode(obj: record) if record
-
end
-
end
-
1
class SaveStatisticJob < ApplicationJob
-
1
queue_as :save_statistic
-
-
1
def perform
-
1
StatisticService.save
-
end
-
end
-
3
class SendEventToBigQueryJob < ApplicationJob
-
3
queue_as :low_priority
-
3
self.logger = ActiveSupport::TaggedLogging.new(Logger.new(IO::NULL))
-
-
3
def perform(event_json, dataset = Settings.google.bigquery.dataset, table = Settings.google.bigquery.table_name)
-
2
return unless FeatureService.enabled?(:send_request_data_to_bigquery)
-
-
1
bq = Google::Cloud::Bigquery.new
-
1
dataset = bq.dataset(dataset, skip_lookup: true)
-
1
bq_table = dataset.table(table, skip_lookup: true)
-
1
bq_table.insert([event_json])
-
end
-
end
-
# frozen_string_literal: true
-
-
4
class ApiConstraint
-
4
def matches?(request)
-
5
Settings.publish_api_url&.include?(request.host)
-
end
-
end
-
4
module BigQuery
-
4
class EntityEvent
-
4
CREATE_ENTITY_EVENT_TYPE = "create_entity".freeze
-
4
UPDATE_ENTITY_EVENT_TYPE = "update_entity".freeze
-
4
IMPORT_EVENT_TYPE = "import_entity".freeze
-
4
EVENT_TYPES = [CREATE_ENTITY_EVENT_TYPE, UPDATE_ENTITY_EVENT_TYPE, IMPORT_EVENT_TYPE].freeze
-
-
4
def initialize
-
@event_hash = {
-
13178
environment: Rails.env,
-
occurred_at: Time.zone.now.iso8601(6),
-
}
-
13178
yield self if block_given?
-
end
-
-
4
delegate :as_json, to: :event_hash
-
-
4
def with_type(type)
-
13175
raise "Invalid analytics event type" unless EVENT_TYPES.include?(type.to_s)
-
-
13174
@event_hash.merge!(
-
event_type: type,
-
)
-
end
-
-
4
def with_entity_table_name(table_name)
-
13173
@event_hash.merge!(
-
entity_table_name: table_name,
-
)
-
end
-
-
4
def with_data(hash)
-
13173
@event_hash.deep_merge!({
-
data: hash_to_kv_pairs(hash),
-
})
-
end
-
-
4
private
-
-
4
attr_reader :event_hash
-
-
4
def hash_to_kv_pairs(hash)
-
13173
hash.map do |(key, value)|
-
195435
value = value.to_s if value.in? [true, false]
-
195435
value = value.to_json if value.is_a?(Hash) || value.is_a?(Array)
-
-
195435
{ "key" => key, "value" => Array.wrap(value) }
-
end
-
end
-
end
-
end
-
2
module BigQuery
-
2
class RequestEvent
-
2
def initialize
-
@event_hash = {
-
15
environment: ENV.fetch("RAILS_ENV", nil),
-
occurred_at: Time.zone.now.iso8601,
-
event_type: "web_request",
-
}
-
15
yield self if block_given?
-
end
-
-
2
delegate :as_json, to: :event_hash
-
-
2
def with_request_details(request)
-
9
event_hash.merge!(
-
request_uuid: request.uuid,
-
request_path: request.path,
-
request_method: request.method,
-
request_user_agent: request.user_agent,
-
request_query: query_to_kv_pairs(request.query_string),
-
request_referer: request.referer,
-
anonymised_user_agent_and_ip: anonymised_user_agent_and_ip(request),
-
)
-
end
-
-
2
def with_response_details(response)
-
4
event_hash.merge!(
-
response_content_type: response.content_type,
-
response_status: response.status,
-
)
-
end
-
-
2
def with_user(user)
-
3
event_hash.merge!(
-
user_id: user&.id,
-
)
-
end
-
-
2
private
-
-
2
attr_reader :event_hash
-
-
2
def query_to_kv_pairs(query_string)
-
9
vars = Rack::Utils.parse_query(query_string)
-
25
vars.map { |k, v| { "key" => k, "value" => [v].flatten } }
-
end
-
-
2
def anonymised_user_agent_and_ip(rack_request)
-
9
if rack_request.remote_ip.present?
-
9
anonymise(rack_request.user_agent.to_s + rack_request.remote_ip.to_s)
-
end
-
end
-
-
2
def anonymise(text)
-
9
Digest::SHA2.hexdigest(text)
-
end
-
end
-
end
-
1
module Govuk
-
1
class MarkdownRenderer < ::Redcarpet::Render::Safe
-
1
def block_html(raw_html)
-
# No user input HTML please
-
end
-
-
1
def raw_html(raw_html)
-
# No user input HTML please
-
end
-
-
1
def emphasis(text)
-
# Disable feature
-
end
-
-
1
def double_emphasis(text)
-
# Disable feature
-
end
-
-
1
def triple_emphasis(text)
-
# Disable feature
-
end
-
-
1
def list(content, list_type)
-
case list_type
-
when :ordered
-
<<~HTML
-
<ol class="govuk-list govuk-list--number">
-
#{content}
-
</ol>
-
HTML
-
when :unordered
-
<<~HTML
-
<ul class="govuk-list govuk-list--bullet">
-
#{content}
-
</ul>
-
HTML
-
end
-
end
-
-
1
def link(link, _title, content)
-
%(<a href="#{link}" class="govuk-link">#{content}</a>)
-
end
-
-
1
def autolink(link, _link_type)
-
%(<a href="#{link}" class="govuk-link">#{link}</a>)
-
end
-
-
1
def paragraph(text)
-
28
%(<p class="govuk-body">#{text}</p>)
-
end
-
-
# Force all headers to <h3> to maintain semantic markup
-
1
def header(text, _heading_level)
-
%(<h3 class="govuk-heading-m">#{text}</h3>)
-
end
-
-
1
def self.render(content)
-
Redcarpet::Markdown.new(self).render(content)
-
end
-
end
-
end
-
4
module TimeFormat
-
# Primarily so that our incremental fetch can handle many records being
-
# updated within the same second.
-
#
-
# The strftime format '%FT%T.%6NZ' is similar to the ISO8601 standard,
-
# (equivalent to %FT%TZ) and adds micro-seconds (%6N).
-
4
def precise_time(time)
-
1
time.strftime("%FT%T.%6NZ")
-
end
-
-
4
def written_month_year(time)
-
242
time.strftime("%B %Y")
-
end
-
-
4
def short_date(time)
-
22
time.strftime("%d/%m/%Y")
-
end
-
-
4
def gov_uk_format(time)
-
75
time.strftime("%-l:%M%P on %-e %B %Y")
-
end
-
end
-
class ApplicationMailer < ActionMailer::Base
-
default from: "from@example.com"
-
layout "mailer"
-
end
-
1
class CoursePublishEmailMailer < GovukNotifyRails::Mailer
-
1
include TimeFormat
-
-
1
def course_publish_email(course, user)
-
9
set_template(Settings.govuk_notify.course_publish_email_template_id)
-
-
9
set_personalisation(
-
provider_name: course.provider.provider_name,
-
course_name: course.name,
-
course_code: course.course_code,
-
course_description: course.description,
-
course_funding_type: course.funding_type,
-
create_course_datetime: gov_uk_format(course.created_at),
-
course_url: create_course_url(course),
-
)
-
-
9
mail(to: user.email)
-
end
-
-
1
private
-
-
1
def create_course_url(course)
-
9
"#{Settings.find_url}" \
-
"/course/#{course.provider.provider_code}" \
-
"/#{course.course_code}"
-
end
-
end
-
2
class CourseSitesUpdateEmailMailer < GovukNotifyRails::Mailer
-
2
include TimeFormat
-
-
2
def course_sites_update_email(
-
course:,
-
previous_site_names:,
-
updated_site_names:,
-
recipient:
-
)
-
9
set_template(Settings.govuk_notify.course_sites_update_email_template_id)
-
-
9
set_personalisation(
-
provider_name: course.provider.provider_name,
-
course_name: course.name,
-
course_code: course.course_code,
-
course_url: create_course_url(course),
-
previous_site_names: previous_site_names.join(", "),
-
updated_site_names: updated_site_names.join(", "),
-
sites_updated_datetime: gov_uk_format(course.updated_at),
-
)
-
-
9
mail(to: recipient.email)
-
end
-
-
2
private
-
-
2
def create_course_url(course)
-
9
"#{Settings.find_url}" \
-
"/course/#{course.provider.provider_code}" \
-
"/#{course.course_code}"
-
end
-
end
-
2
class CourseSubjectsUpdatedEmailMailer < GovukNotifyRails::Mailer
-
2
include TimeFormat
-
-
2
def course_subjects_updated_email(
-
course:,
-
previous_subject_names:,
-
previous_course_name:,
-
recipient:
-
)
-
-
10
set_template(Settings.govuk_notify.course_subjects_updated_email_template_id)
-
-
10
set_personalisation(
-
provider_name: course.provider.provider_name,
-
course_code: course.course_code,
-
subject_change_datetime: gov_uk_format(course.updated_at),
-
course_url: create_course_url(course),
-
previous_subjects: format(previous_subject_names),
-
updated_subjects: format(course.subjects.map(&:subject_name)),
-
previous_course_name: previous_course_name,
-
updated_course_name: course.name,
-
)
-
-
10
mail(to: recipient.email)
-
end
-
-
2
private
-
-
2
def format(subject_names)
-
20
subject_names.join(", ")
-
end
-
-
2
def create_course_url(course)
-
10
"#{Settings.find_url}" \
-
"/course/#{course.provider.provider_code}" \
-
"/#{course.course_code}"
-
end
-
end
-
1
class CourseUpdateEmailMailer < GovukNotifyRails::Mailer
-
1
include TimeFormat
-
-
1
def course_update_email(
-
course:,
-
attribute_name:,
-
original_value:,
-
updated_value:,
-
recipient:
-
)
-
19
set_template(Settings.govuk_notify.course_update_email_template_id)
-
-
19
set_personalisation(
-
provider_name: course.provider.provider_name,
-
course_name: set_course_name(course, attribute_name, original_value),
-
course_code: course.course_code,
-
course_description: course.description,
-
course_funding_type: course.funding_type,
-
attribute_changed: I18n.t("course.update_email.#{attribute_name}"),
-
attribute_change_datetime: gov_uk_format(course.updated_at),
-
course_url: create_course_url(course),
-
original_value: formatter.call(name: attribute_name, value: original_value),
-
updated_value: formatter.call(name: attribute_name, value: updated_value),
-
)
-
-
19
mail(to: recipient.email)
-
end
-
-
1
private
-
-
1
def create_course_url(course)
-
19
"#{Settings.find_url}" \
-
"/course/#{course.provider.provider_code}" \
-
"/#{course.course_code}"
-
end
-
-
1
def formatter
-
38
CourseAttributeFormatterService
-
end
-
-
1
def set_course_name(course, attribute_name, original)
-
19
return original if attribute_name == "name"
-
-
18
course.name
-
end
-
end
-
2
module CourseVacancies
-
2
class UpdatedMailer < GovukNotifyRails::Mailer
-
2
include TimeFormat
-
-
2
def fully_updated(course:, user:, datetime:, vacancies_filled:)
-
11
set_template(Settings.govuk_notify.course_vacancies_updated_email_template_id)
-
-
11
set_personalisation(
-
provider_name: course.provider.provider_name,
-
course_name: course.name,
-
course_code: course.course_code,
-
vacancies_updated_datetime: gov_uk_format(datetime),
-
course_url: create_course_url(course),
-
11
vacancies_filled: vacancies_filled ? "yes" : "no",
-
11
vacancies_opened: vacancies_filled ? "no" : "yes",
-
)
-
-
11
mail(to: user.email)
-
end
-
-
2
def partially_updated(course:, user:, datetime:, vacancies_opened:, vacancies_closed:)
-
9
set_template(Settings.govuk_notify.course_vacancies_partially_updated_email_template_id)
-
-
9
set_personalisation(
-
provider_name: course.provider.provider_name,
-
course_name: course.name,
-
course_code: course.course_code,
-
vacancies_updated_datetime: gov_uk_format(datetime),
-
course_url: create_course_url(course),
-
9
vacancies_opened: vacancies_opened ? vacancies_opened.join(", ") : "none",
-
9
vacancies_closed: vacancies_closed ? vacancies_closed.join(", ") : "none",
-
)
-
-
9
mail(to: user.email)
-
end
-
-
2
private
-
-
2
def create_course_url(course)
-
20
"#{Settings.find_url}" \
-
"/course/#{course.provider.provider_code}" \
-
"/#{course.course_code}"
-
end
-
end
-
end
-
2
class CourseWithdrawEmailMailer < GovukNotifyRails::Mailer
-
2
include TimeFormat
-
-
2
def course_withdraw_email(course, user, datetime)
-
6
set_template(Settings.govuk_notify.course_withdraw_email_template_id)
-
-
6
set_personalisation(
-
provider_name: course.provider.provider_name,
-
course_name: course.name,
-
course_code: course.course_code,
-
withdraw_course_datetime: gov_uk_format(datetime),
-
)
-
-
6
mail(to: user.email)
-
end
-
end
-
2
class MagicLinkEmailMailer < GovukNotifyRails::Mailer
-
2
def magic_link_email(user)
-
4
set_template(Settings.govuk_notify.magic_link_email_template_id)
-
-
4
set_personalisation(
-
first_name: user.first_name,
-
magic_link_url: magic_link_url_for_user(user),
-
)
-
-
4
mail(to: user.email)
-
end
-
-
2
private
-
-
2
def magic_link_url_for_user(user)
-
4
"#{Settings.base_url}/signin_with_magic_link?email=#{user.email}&token=#{user.magic_link_token}"
-
end
-
end
-
2
class WelcomeEmailMailer < GovukNotifyRails::Mailer
-
2
class MissingFirstNameError < StandardError; end
-
-
2
def send_welcome_email(first_name:, email:)
-
# Getting visibility on the missing personalisation first_name issue
-
4
raise MissingFirstNameError, "You must provide a firstname personalisation." if first_name.blank?
-
-
3
set_template(Settings.govuk_notify.welcome_email_template_id)
-
-
3
set_personalisation(
-
first_name: first_name,
-
)
-
-
3
mail(to: email)
-
rescue MissingFirstNameError => e
-
1
Sentry.capture_exception(e)
-
end
-
end
-
4
class AccessRequest < ApplicationRecord
-
4
include Discard::Model
-
236
default_scope -> { kept }
-
-
4
belongs_to :requester, class_name: "User"
-
-
4
enum status: %i[
-
requested
-
approved
-
completed
-
declined
-
].freeze
-
-
6
scope :by_request_date, -> { order(request_date_utc: :asc) }
-
-
4
def recipient
-
6
User.new(first_name: first_name, last_name: last_name, email: email_address)
-
end
-
-
4
def add_additional_attributes(requester_email)
-
3
update(requester: User.find_by(email: requester_email),
-
request_date_utc: Time.now.utc,
-
status: :requested)
-
end
-
-
4
alias_method :approve, :completed!
-
-
4
validates :first_name, :last_name, :email_address,
-
:organisation, :reason, :requester_email,
-
presence: true
-
-
4
audited
-
end
-
4
class AccreditingProviderEnrichment
-
4
include ActiveModel::Validations
-
4
include ActiveModel::Model
-
-
# Pascal cased as the original is stored like so.
-
4
attr_accessor :UcasProviderCode, :Description
-
-
4
validates :Description, words_count: { maximum: 100 }
-
-
4
def initialize(attrs)
-
117
attrs.each do |attr, value|
-
266
send("#{attr}=", value) unless attr == "errors"
-
end
-
end
-
-
4
def attributes
-
%i[UcasProviderCode Description].inject({}) do |hash, attr|
-
hash[attr] = send(attr)
-
hash
-
end
-
end
-
-
4
class ArraySerializer
-
4
class << self
-
4
def load(json)
-
9322
if json.present?
-
127
arr = JSON.parse json
-
-
127
arr.map do |item|
-
117
AccreditingProviderEnrichment.new(item)
-
end
-
end
-
end
-
-
4
def dump(obj)
-
141
obj&.to_json
-
end
-
end
-
end
-
end
-
4
class Allocation < ApplicationRecord
-
4
ALLOCATION_CYCLE_YEAR = Settings.allocation_cycle_year.to_s.freeze
-
-
4
include PgSearch::Model
-
-
4
belongs_to :provider
-
4
belongs_to :accredited_body, class_name: "Provider"
-
4
belongs_to :recruitment_cycle
-
4
has_one :allocation_uplift
-
-
4
audited associated_with: :provider
-
-
4
validates :number_of_places, numericality: true
-
-
4
validate :accredited_body_is_an_accredited_body
-
4
validate :non_zero_initial_request
-
-
4
enum request_type: { initial: 0, repeat: 1, declined: 2 }
-
-
31
scope :current_allocations, -> { where(recruitment_cycle: RecruitmentCycle.find_by(year: Settings.allocation_cycle_year)) }
-
-
4
pg_search_scope :search, associated_against: {
-
provider: %i[provider_code provider_name],
-
accredited_body: %i[provider_code provider_name],
-
}, using: { tsearch: { prefix: true } }
-
-
4
def previous
-
7
Allocation.find_by(
-
provider_code: provider_code,
-
accredited_body_code: accredited_body_code,
-
recruitment_cycle: recruitment_cycle.previous,
-
)
-
end
-
-
4
def self.journey_mode
-
{
-
50
"open" => "open",
-
"closed" => "closed",
-
"confirmed" => "confirmed",
-
}.fetch(Settings.features.allocations.state, "open")
-
end
-
-
4
private
-
-
4
def accredited_body_is_an_accredited_body
-
97
errors.add(:accredited_body, "must be an accredited body") unless accredited_body&.accredited_body?
-
end
-
-
4
def non_zero_initial_request
-
97
errors.add(:number_of_places, "must not be zero") if request_type == "initial" && number_of_places.zero?
-
end
-
end
-
4
class AllocationUplift < ApplicationRecord
-
4
belongs_to :allocation
-
-
4
delegate :provider, :recruitment_cycle, to: :allocation
-
-
4
validates :uplifts, numericality: true
-
end
-
# Wrapper for our API Specs
-
#
-
# This concern contains methods to operate on our API specs, primarily defined
-
# as OpenAPI specs.
-
module APISpec
-
extend ActiveSupport::Concern
-
-
class_methods do
-
def latest
-
version(latest_version_number)
-
end
-
-
def version(version)
-
new(format(openapi_file_path, version: version))
-
end
-
end
-
-
attr_reader :openapi_spec_file
-
-
def initialize(path)
-
@openapi_spec_file = path
-
end
-
-
def to_openapi
-
Openapi3Parser.load(to_yaml)
-
end
-
-
def to_yaml
-
@to_yaml ||= JSON.parse(File.read(@openapi_spec_file)).to_yaml
-
end
-
end
-
module APISpec
-
# Define the version and location of our public spec.
-
class Public
-
# This concern has all the goodness used externally.
-
include APISpec
-
-
class << self
-
def latest_version_number
-
1
-
end
-
-
def openapi_file_path
-
"swagger/v%{version}/swagger.json"
-
end
-
end
-
end
-
end
-
4
class ApplicationRecord < ActiveRecord::Base
-
4
self.abstract_class = true
-
4
include EmitsEntityEvents
-
end
-
4
module ChangedAt
-
4
extend ActiveSupport::Concern
-
-
4
class_methods do
-
4
private
-
-
# Hook into Rails' built-in mechanism to update `updated_at` by adding to
-
# it's list of columns that get updated when an object changes (by default
-
# this is 'updated_at' and 'updated_on'). This is simpler than using a
-
# before/after save hook and should allow our 'changed_at' to behave in
-
# exactly the same way as 'updated_at'.
-
4
def timestamp_attributes_for_update
-
8
super + %w[changed_at]
-
end
-
end
-
end
-
4
module Courses
-
4
module EditOptions
-
4
extend ActiveSupport::Concern
-
4
include AgeRangeConcern
-
4
include EntryRequirementsConcern
-
4
include QualificationConcern
-
4
include StartDateConcern
-
4
include StudyModeConcern
-
4
include IsSendConcern
-
4
include ApplicationsOpenConcern
-
4
include SubjectsConcern
-
-
4
included do
-
# When changing edit options here be sure to update the edit_options in the
-
# courses factory in publish-teacher-training:
-
#
-
# https://github.com/DFE-Digital/publish-teacher-training/blob/master/spec/factories/courses.rb
-
4
def edit_course_options
-
{
-
183
entry_requirements: entry_requirements,
-
qualifications: qualification_options,
-
age_range_in_years: age_range_options,
-
start_dates: start_date_options,
-
study_modes: study_mode_options,
-
show_is_send: show_is_send?,
-
show_start_date: show_start_date?,
-
show_applications_open: show_applications_open?,
-
subjects: potential_subjects,
-
modern_languages: modern_languages,
-
modern_languages_subject: modern_languages_subject,
-
}.with_indifferent_access
-
end
-
end
-
end
-
end
-
4
module Courses
-
4
module EditOptions
-
4
module AgeRangeConcern
-
4
extend ActiveSupport::Concern
-
4
included do
-
# When changing anything here be sure to update the edit_options in the
-
# courses factory in publish-teacher-training:
-
#
-
# https://github.com/DFE-Digital/publish-teacher-training/blob/master/spec/factories/courses.rb
-
8
def age_range_options
-
197
case level
-
when "primary"
-
%w[
-
122
3_to_7
-
5_to_11
-
7_to_11
-
7_to_14
-
]
-
when "secondary"
-
%w[
-
69
11_to_16
-
11_to_18
-
14_to_19
-
]
-
end
-
end
-
end
-
end
-
end
-
end
-
4
module Courses
-
4
module EditOptions
-
4
module ApplicationsOpenConcern
-
4
extend ActiveSupport::Concern
-
4
included do
-
# When changing anything here be sure to update the edit_options in the
-
# courses factory in publish-teacher-training:
-
#
-
# https://github.com/DFE-Digital/publish-teacher-training/blob/master/spec/factories/courses.rb
-
4
def show_applications_open?
-
185
!is_published?
-
end
-
end
-
end
-
end
-
end
-
4
module Courses
-
4
module EditOptions
-
4
module EntryRequirementsConcern
-
4
extend ActiveSupport::Concern
-
4
included do
-
# When changing anything here be sure to update the edit_options in the
-
# courses factory in publish-teacher-training:
-
#
-
# https://github.com/DFE-Digital/publish-teacher-training/blob/master/spec/factories/courses.rb
-
5
def entry_requirements
-
185
Course::ENTRY_REQUIREMENT_OPTIONS
-
.keys
-
925
.reject { |option| option.in? %i[not_set not_required] }
-
end
-
end
-
end
-
end
-
end
-
4
module Courses
-
4
module EditOptions
-
4
module IsSendConcern
-
4
extend ActiveSupport::Concern
-
4
included do
-
# When changing anything here be sure to update the edit_options in the
-
# courses factory in publish-teacher-training:
-
#
-
# https://github.com/DFE-Digital/publish-teacher-training/blob/master/spec/factories/courses.rb
-
4
def show_is_send?
-
185
!is_published?
-
end
-
end
-
end
-
end
-
end
-
4
module Courses
-
4
module EditOptions
-
4
module QualificationConcern
-
4
extend ActiveSupport::Concern
-
4
included do
-
# When changing anything here be sure to update the edit_options in the
-
# courses factory in publish-teacher-training:
-
#
-
# https://github.com/DFE-Digital/publish-teacher-training/blob/master/spec/factories/courses.rb
-
6
def qualification_options
-
410
if level == "further_education"
-
15
qualifications_without_qts
-
else
-
395
qualifications_with_qts
-
end
-
end
-
-
6
def qualifications_with_qts
-
395
Course.qualifications.keys.select do |qualification|
-
1975
qualification.include?("qts")
-
end
-
end
-
-
6
def qualifications_without_qts
-
15
Course.qualifications.keys.reject do |qualification|
-
75
qualification.include?("qts")
-
end
-
end
-
end
-
end
-
end
-
end
-
4
module Courses
-
4
module EditOptions
-
4
module StartDateConcern
-
4
extend ActiveSupport::Concern
-
4
included do
-
# When changing anything here be sure to update the edit_options in the
-
# courses factory in publish-teacher-training:
-
#
-
# https://github.com/DFE-Digital/publish-teacher-training/blob/master/spec/factories/courses.rb
-
5
def start_date_options
-
329
recruitment_year = provider.recruitment_cycle.year.to_i
-
-
329
["October #{recruitment_year - 1}",
-
"November #{recruitment_year - 1}",
-
"December #{recruitment_year - 1}",
-
"January #{recruitment_year}",
-
"February #{recruitment_year}",
-
"March #{recruitment_year}",
-
"April #{recruitment_year}",
-
"May #{recruitment_year}",
-
"June #{recruitment_year}",
-
"July #{recruitment_year}",
-
"August #{recruitment_year}",
-
"September #{recruitment_year}",
-
"October #{recruitment_year}",
-
"November #{recruitment_year}",
-
"December #{recruitment_year}",
-
"January #{recruitment_year + 1}",
-
"February #{recruitment_year + 1}",
-
"March #{recruitment_year + 1}",
-
"April #{recruitment_year + 1}",
-
"May #{recruitment_year + 1}",
-
"June #{recruitment_year + 1}",
-
"July #{recruitment_year + 1}"]
-
end
-
-
5
def show_start_date?
-
185
!is_published?
-
end
-
end
-
end
-
end
-
end
-
4
module Courses
-
4
module EditOptions
-
4
module StudyModeConcern
-
4
extend ActiveSupport::Concern
-
4
included do
-
# When changing anything here be sure to update the edit_options in the
-
# courses factory in publish-teacher-training:
-
#
-
# https://github.com/DFE-Digital/publish-teacher-training/blob/master/spec/factories/courses.rb
-
4
def study_mode_options
-
183
Course.study_modes.keys
-
end
-
end
-
end
-
end
-
end
-
4
module Courses
-
4
module EditOptions
-
4
module SubjectsConcern
-
4
extend ActiveSupport::Concern
-
4
included do
-
# When changing anything here be sure to update the edit_options in the
-
# courses factory in publish-teacher-training:
-
#
-
# https://github.com/DFE-Digital/publish-teacher-training/blob/master/spec/factories/courses.rb
-
4
def potential_subjects
-
185
assignable_master_subjects&.sort_by(&:subject_name)
-
end
-
-
4
def modern_languages
-
184
ModernLanguagesSubject.all
-
end
-
-
4
def modern_languages_subject
-
184
SecondarySubject.modern_languages
-
end
-
end
-
end
-
end
-
end
-
4
module EmitsEntityEvents
-
4
extend ActiveSupport::Concern
-
-
4
included do
-
4
attr_accessor :event_tags
-
-
4
after_create do
-
13019
data = entity_data(attributes)
-
13019
send_event(build_event(BigQuery::EntityEvent::CREATE_ENTITY_EVENT_TYPE, data)) if data.any?
-
end
-
-
4
after_update do
-
# in this after_update hook we don’t have access to the new fields via
-
# #attributes — we need to dig them out of saved_changes which stores
-
# them in the format { attr: ['old', 'new'] }
-
-
844
interesting_changes = entity_data(saved_changes.transform_values(&:last))
-
-
844
if interesting_changes.any?
-
634
send_event(
-
build_event(BigQuery::EntityEvent::UPDATE_ENTITY_EVENT_TYPE, entity_data(attributes).merge(interesting_changes)),
-
)
-
end
-
end
-
end
-
-
4
def send_import_event
-
2
build_event(BigQuery::EntityEvent::IMPORT_EVENT_TYPE, entity_data(attributes)).tap do |event|
-
2
send_event(event)
-
end
-
end
-
-
4
private
-
-
4
def send_event(event)
-
13172
return unless FeatureService.enabled?(:send_request_data_to_bigquery)
-
-
5
SendEventToBigQueryJob.perform_later(event.as_json)
-
end
-
-
4
def entity_data(changeset)
-
14499
exportable_attrs = Rails.configuration.analytics[self.class.table_name.to_sym]
-
14499
changeset.slice(*exportable_attrs&.map(&:to_s))
-
end
-
-
4
def build_event(type, data)
-
13172
BigQuery::EntityEvent.new do |ee|
-
13172
ee.with_type(type)
-
13172
ee.with_entity_table_name(self.class.table_name)
-
13172
ee.with_data(data)
-
end
-
end
-
end
-
4
module PostcodeNormalize
-
4
extend ActiveSupport::Concern
-
4
included do
-
4
def postcode=(str)
-
732
if str
-
729
super UKPostcode.parse(str).to_s
-
else
-
3
super str
-
end
-
end
-
end
-
end
-
4
module Publish
-
4
module CourseBasicDetailConcern
-
4
extend ActiveSupport::Concern
-
-
4
included do
-
32
decorates_assigned :course
-
32
before_action :build_new_course, only: %i[back new continue]
-
32
before_action :build_previous_course_creation_params, only: %i[new continue]
-
32
before_action :build_meta_course_creation_params, only: %i[new continue]
-
32
before_action :build_back_link, only: %i[new back continue]
-
32
before_action :build_course, only: %i[edit update]
-
end
-
-
4
def back; end
-
-
4
def new
-
64
authorize(@provider, :can_create_course?)
-
end
-
-
4
def edit
-
3
authorize(provider)
-
end
-
-
4
def update
-
authorize(provider)
-
-
@errors = errors
-
return render :edit if @errors.present?
-
-
if @course.update(course_params)
-
flash[:success] = I18n.t("success.saved")
-
redirect_to(
-
details_publish_provider_recruitment_cycle_course_path(
-
@course.provider_code,
-
@course.recruitment_cycle_year,
-
@course.course_code,
-
),
-
)
-
else
-
@errors = @course.errors.messages
-
render :edit
-
end
-
end
-
-
4
def continue
-
45
authorize(@provider, :can_create_course?)
-
45
@errors = errors
-
-
45
if @errors.any?
-
10
render :new
-
else
-
35
redirect_to next_step
-
end
-
end
-
-
4
private
-
-
4
def build_new_course
-
130
add_custom_age_range_into_params if params.dig("course", "age_range_in_years") == "other"
-
-
130
@course = ::Courses::CreationService.call(course_params: course_params, provider: provider)
-
end
-
-
4
def build_course
-
26
@course = provider.courses.find_by!(course_code: params[:code])
-
end
-
-
4
def add_custom_age_range_into_params
-
1
params["course"]["age_range_in_years"] = "#{age_from_param}_to_#{age_to_param}"
-
end
-
-
4
def errors
-
271
@course.errors.messages.select { |key, _message| error_keys.include?(key) }
-
end
-
-
4
def error_keys
-
[]
-
end
-
-
4
def course_params
-
407
if params.key? :course
-
393
params.require(:course)
-
.except(
-
:day,
-
:month,
-
:year,
-
:course_age_range_in_years_other_from,
-
:course_age_range_in_years_other_to,
-
:goto_confirmation,
-
:language_ids,
-
).permit(
-
policy(Course.new).permitted_new_course_attributes,
-
sites_ids: [],
-
subjects_ids: [],
-
)
-
else
-
14
ActionController::Parameters.new({}).permit(:course)
-
end
-
end
-
-
4
def build_previous_course_creation_params
-
123
@course_creation_params = course_params
-
end
-
-
4
def build_meta_course_creation_params
-
123
@meta_course_creation_params = params.slice(:goto_confirmation)
-
end
-
-
4
def continue_step
-
39
if params[:goto_confirmation] && current_step != :subjects
-
:confirmation
-
else
-
39
CourseCreationStepService.new.execute(current_step: current_step, course: @course)[:next]
-
end
-
end
-
-
4
def next_step
-
39
continue_path = course_creation_path_for(continue_step)
-
-
39
if continue_path.nil?
-
raise "No path defined for continue step: #{continue_path}"
-
end
-
-
39
continue_path
-
end
-
-
4
def path_params
-
149
path_params = { course: course_params }
-
149
path_params[:goto_confirmation] = params[:goto_confirmation] if params[:goto_confirmation]
-
149
path_params
-
end
-
-
4
def back_step
-
123
if params[:goto_confirmation]
-
if current_step == :modern_languages
-
:subjects
-
else
-
:confirmation
-
end
-
else
-
123
CourseCreationStepService.new.execute(current_step: current_step, course: @course)[:previous]
-
end
-
end
-
-
4
def build_back_link
-
123
previous_path = course_back_path_for(back_step)
-
-
123
if previous_path.nil?
-
raise "No path defined for back step: #{back_step}"
-
end
-
-
123
@back_link_path = previous_path
-
end
-
-
4
def course_back_path_for(page)
-
123
case page
-
when :location
-
7
back_publish_provider_recruitment_cycle_courses_locations_path(path_params)
-
when :modern_languages
-
12
back_publish_provider_recruitment_cycle_courses_modern_languages_path(path_params)
-
else
-
104
course_creation_path_for(page)
-
end
-
end
-
-
4
def course_creation_path_for(page)
-
143
case page
-
when :courses_list
-
13
publish_provider_recruitment_cycle_courses_path(@provider.provider_code, @provider.recruitment_cycle_year)
-
when :level
-
14
new_publish_provider_recruitment_cycle_courses_level_path(path_params)
-
when :modern_languages
-
4
new_publish_provider_recruitment_cycle_courses_modern_languages_path(path_params)
-
when :apprenticeship
-
5
new_publish_provider_recruitment_cycle_courses_apprenticeship_path(path_params)
-
when :location
-
4
new_publish_provider_recruitment_cycle_courses_locations_path(path_params)
-
when :entry_requirements
-
new_publish_provider_recruitment_cycle_courses_entry_requirements_path(path_params)
-
when :outcome
-
23
new_publish_provider_recruitment_cycle_courses_outcome_path(path_params)
-
when :full_or_part_time
-
15
new_publish_provider_recruitment_cycle_courses_study_mode_path(path_params)
-
when :applications_open
-
8
new_publish_provider_recruitment_cycle_courses_applications_open_path(path_params)
-
when :accredited_body
-
8
new_publish_provider_recruitment_cycle_courses_accredited_body_path(path_params)
-
when :start_date
-
3
new_publish_provider_recruitment_cycle_courses_start_date_path(path_params)
-
when :age_range
-
17
new_publish_provider_recruitment_cycle_courses_age_range_path(path_params)
-
when :subjects
-
13
new_publish_provider_recruitment_cycle_courses_subjects_path(path_params)
-
when :fee_or_salary
-
14
new_publish_provider_recruitment_cycle_courses_fee_or_salary_path(path_params)
-
when :confirmation
-
2
confirmation_publish_provider_recruitment_cycle_courses_path(path_params)
-
end
-
end
-
end
-
end
-
4
module RegionCode
-
4
extend ActiveSupport::Concern
-
4
included do
-
# These correspond to the first-level NUTS regions for the UK (minus Northern Ireland)
-
# https://en.wikipedia.org/wiki/First-level_NUTS_of_the_European_Union#United_Kingdom
-
-
8
enum region_code: {
-
no_region: 0,
-
london: 1,
-
south_east: 2,
-
south_west: 3,
-
wales: 4,
-
west_midlands: 5,
-
east_midlands: 6,
-
eastern: 7,
-
north_west: 8,
-
yorkshire_and_the_humber: 9,
-
north_east: 10,
-
scotland: 11,
-
}
-
end
-
end
-
4
module TouchCourse
-
4
extend ActiveSupport::Concern
-
-
4
included do
-
8
after_save :touch_course
-
end
-
-
4
private
-
-
4
def touch_course
-
1088
course.update_changed_at
-
end
-
end
-
4
module TouchProvider
-
4
extend ActiveSupport::Concern
-
-
4
included do
-
12
after_save :touch_provider
-
end
-
-
4
private
-
-
4
def touch_provider
-
3663
provider.update_changed_at
-
end
-
end
-
4
module WithQualifications
-
4
extend ActiveSupport::Concern
-
-
4
included do
-
# The training programme outcome that originated in the UCAS NetUpdate system.
-
#
-
# See [UCAS Teacher Training Set-up Guide](https://www.ucas.com/file/115581/download?token=mv-G6P53),
-
# page 32
-
4
enum profpost_flag: {
-
-
# Recommendation for QTS: the student will not receive an academic teacher
-
# training qualification on successful completion of the
-
# programme.
-
recommendation_for_qts: "",
-
-
# Professional: the student will receive a Professional Graduate
-
# Certificate of Education (offered at Level 6) or Professional
-
# Graduate Diploma in Education (PGDE), with no credits or
-
# modules at postgraduate (master's) Level 7 on successful
-
# completion of the programme.
-
professional: "PF",
-
-
# Postgraduate: the student will receive a Postgraduate Certificate
-
# of Education (PGCE) or other qualification which includes at
-
# least one module or some credits at postgraduate
-
# (master's) Level 7 on successful completion the
-
# programme.
-
postgraduate: "PG",
-
-
# Both professional and postgraduate: the student has the option of taking at least one
-
# postgraduate (master's) Level 7 module or obtaining some
-
# postgraduate-level credits as part of the programme.
-
professional_postgraduate: "BO",
-
}
-
-
# When UCAS basic courses were being imported into Manage Courses DB, this
-
# field wasn't coming from the UCAS data. Instead, the UCAS importer derived
-
# this field from the `profpost_flag`, the `pgde_course` table and from the
-
# subjects this course was tagged to.
-
#
-
# Defined here: https://github.com/DFE-Digital/manage-courses-api/blob/master/src/ManageCourses.Domain/Models/CourseQualification.cs
-
4
enum qualification: { qts: 0, pgce_with_qts: 1, pgde_with_qts: 2, pgce: 3, pgde: 4 }
-
-
# This field may seem like an unnecessary overhead when there is already a
-
# database-backed `qualification` field. However it's misleading, from the
-
# point of view of the teacher training domain, to think of 'PGCE with QTS'
-
# as a single qualification, since the QTS and PGCE aspects are completely
-
# separate and may even be delivered in different places by different providers.
-
# e.g. the QTS might come from a SCITT but the PGCE would come from a university.
-
#
-
# It's more accurate (and hopefully more future-proof) to represent qualifications
-
# as a list and leave the gluing of them to the presentation layer.
-
4
def qualifications
-
757
case qualification
-
18
when "qts" then [:qts]
-
709
when "pgce_with_qts" then %i[qts pgce]
-
16
when "pgde_with_qts" then %i[qts pgde]
-
8
when "pgce" then [:pgce]
-
4
when "pgde" then [:pgde]
-
end
-
end
-
-
4
def qualifications_description
-
324
return "" unless qualifications
-
-
322
qualifications.map(&:upcase).sort.join(" with ")
-
end
-
-
4
def qualification=(value)
-
2457
super(value)
-
2457
self.profpost_flag = if qts?
-
70
:recommendation_for_qts
-
else
-
2387
:postgraduate
-
end
-
end
-
end
-
end
-
4
class Contact < ApplicationRecord
-
4
self.inheritance_column = "_unused"
-
-
4
include TouchProvider
-
-
4
belongs_to :provider
-
-
4
audited associated_with: :provider
-
-
4
validates :name, presence: true
-
4
validates :email, email_address: true, presence: true
-
4
validates :telephone, phone: true, allow_nil: true
-
4
validates :permission_given, acceptance: true
-
-
4
enum type: {
-
admin: "admin",
-
utt: "utt",
-
web_link: "web_link",
-
fraud: "fraud",
-
finance: "finance",
-
},
-
_suffix: "contact"
-
end
-
4
class Course < ApplicationRecord
-
4
include Discard::Model
-
4
include WithQualifications
-
4
include ChangedAt
-
4
include TouchProvider
-
4
include Courses::EditOptions
-
4
include StudyModeVacancyMapper
-
4
include TimeFormat
-
-
4
after_initialize :set_defaults
-
-
4
before_discard do
-
10
raise "You cannot delete the running course #{self}" unless %i[new not_running].include?(ucas_status)
-
end
-
-
4
has_associated_audits
-
4
audited
-
-
4
validates :course_code,
-
uniqueness: { scope: :provider_id },
-
presence: true,
-
on: %i[create update]
-
-
4
enum program_type: {
-
higher_education_programme: "HE",
-
school_direct_training_programme: "SD",
-
school_direct_salaried_training_programme: "SS",
-
scitt_programme: "SC",
-
pg_teaching_apprenticeship: "TA",
-
}
-
-
4
enum study_mode: {
-
full_time: "F",
-
part_time: "P",
-
full_time_or_part_time: "B",
-
}
-
-
4
enum level: {
-
primary: "Primary",
-
secondary: "Secondary",
-
further_education: "Further education",
-
}, _suffix: :course
-
-
4
enum degree_grade: {
-
two_one: 0,
-
two_two: 1,
-
third_class: 2,
-
not_required: 9,
-
}
-
-
4
ENTRY_REQUIREMENT_OPTIONS = {
-
must_have_qualification_at_application_time: 1,
-
expect_to_achieve_before_training_begins: 2,
-
equivalence_test: 3,
-
not_required: 9,
-
not_set: nil,
-
}.freeze
-
-
4
STRUCTURED_REQUIREMENTS_REQUIRED_FROM = 2022
-
-
# Most providers require GCSE grade 4 ("C"),
-
# but some require grade 5 ("strong C")
-
4
PROVIDERS_REQUIRING_GCSE_GRADE_5 = %w[U80 I30].freeze
-
-
4
enum maths: ENTRY_REQUIREMENT_OPTIONS, _suffix: :for_maths
-
4
enum english: ENTRY_REQUIREMENT_OPTIONS, _suffix: :for_english
-
4
enum science: ENTRY_REQUIREMENT_OPTIONS, _suffix: :for_science
-
-
4
after_validation :remove_unnecessary_enrichments_validation_message
-
4
before_save :set_applications_open_from
-
-
4
belongs_to :provider
-
-
4
belongs_to :accrediting_provider,
-
65
->(c) { where(recruitment_cycle: c.recruitment_cycle) },
-
class_name: "Provider",
-
foreign_key: :accredited_body_code,
-
primary_key: :provider_code,
-
inverse_of: :accredited_courses,
-
optional: true
-
-
4
has_many :course_subjects,
-
5869
-> { order :position },
-
inverse_of: :course,
-
before_add: :set_subject_position
-
-
4
delegate :recruitment_cycle, :provider_code, to: :provider, allow_nil: true
-
4
delegate :after_2021?, :year, to: :recruitment_cycle, allow_nil: true, prefix: :recruitment_cycle
-
-
4
def set_subject_position(course_subject)
-
2677
return unless course_subject.subject.secondary_subject?
-
-
323
secondary_course_subjects = course_subjects.select { |cs| cs.subject.secondary_subject? }
-
-
321
return unless secondary_course_subjects.all? { |cs| cs.position.present? }
-
-
279
course_subject.position = if secondary_course_subjects.any?
-
39
secondary_course_subjects.last.position + 1
-
else
-
240
0
-
end
-
end
-
-
4
has_many :subjects, through: :course_subjects
-
4
has_many :financial_incentives, through: :subjects
-
4
has_many :site_statuses
-
4
accepts_nested_attributes_for :site_statuses
-
4
has_many :sites,
-
512
-> { distinct.merge(SiteStatus.where(status: %i[new_status running])) },
-
through: :site_statuses
-
-
4
has_many :modern_languages_subjects,
-
through: :course_subjects,
-
source: :subject,
-
class_name: "ModernLanguagesSubject"
-
-
4
has_many :enrichments, class_name: "CourseEnrichment" do
-
4
def find_or_initialize_draft
-
# This is a ruby search as opposed to an AR search, because calling `draft`
-
# will return a new instance of a CourseEnrichment object which is different
-
# to the ones in the cached `enrichments` association. This makes checking
-
# for validations later down non-trivial.
-
61
latest_draft_enrichment = select(&:draft?).last
-
-
61
latest_draft_enrichment || new(new_draft_attributes)
-
end
-
-
4
def new_draft_attributes
-
47
latest_published_enrichment = most_recent.published.first
-
-
47
if latest_published_enrichment.present?
-
27
latest_published_enrichment_attributes = latest_published_enrichment
-
.dup
-
.attributes
-
.with_indifferent_access
-
.except(:json_data)
-
-
27
latest_published_enrichment_attributes[:status] = :draft
-
27
latest_published_enrichment_attributes
-
else
-
20
{ status: :draft }.with_indifferent_access
-
end
-
end
-
end
-
-
120
has_one :latest_published_enrichment, -> { published.order("created_at DESC, id DESC").limit(1) },
-
class_name: "CourseEnrichment"
-
-
4
scope :within, lambda { |range, origin:|
-
4
joins(site_statuses: :site).merge(SiteStatus.where(status: :running)).merge(Site.within(range, origin: origin))
-
}
-
-
4
scope :by_name_ascending, lambda {
-
1
order(name: :asc)
-
}
-
-
4
scope :by_name_descending, lambda {
-
1
order(name: :desc)
-
}
-
-
4
scope :ascending_canonical_order, lambda {
-
10
joins(:provider).merge(Provider.by_name_ascending).order("name asc, course_code asc")
-
}
-
-
4
scope :descending_canonical_order, lambda {
-
4
joins(:provider).merge(Provider.by_name_descending).order("name desc, course_code desc")
-
}
-
-
4
scope :accredited_body_order, lambda { |provider_name|
-
1
joins(:provider).merge(Provider.by_provider_name(provider_name))
-
}
-
-
4
scope :case_insensitive_search, lambda { |course_code|
-
1
where("lower(course.course_code) = ?", course_code.downcase)
-
}
-
-
4
scope :changed_since, lambda { |timestamp|
-
17
if timestamp.present?
-
15
changed_at_since(timestamp)
-
else
-
2
where.not(changed_at: nil)
-
end.order(:changed_at, :id)
-
}
-
-
4
scope :changed_at_since, lambda { |timestamp|
-
26
where("course.changed_at > ?", timestamp)
-
}
-
-
4
scope :created_at_since, lambda { |timestamp|
-
16
where("course.created_at > ?", timestamp)
-
}
-
-
4
scope :not_new, lambda {
-
3
includes(site_statuses: %i[site course])
-
.where
-
.not(SiteStatus.table_name => { status: SiteStatus.statuses[:new_status] })
-
}
-
-
4
scope :published, lambda {
-
6
where(id: CourseEnrichment.published.select(:course_id))
-
}
-
-
6
scope :with_recruitment_cycle, ->(year) { joins(provider: :recruitment_cycle).where(recruitment_cycle: { year: year }) }
-
160
scope :findable, -> { joins(:site_statuses).merge(SiteStatus.findable) }
-
44
scope :with_vacancies, -> { joins(:site_statuses).merge(SiteStatus.with_vacancies) }
-
9
scope :with_salary, -> { where(program_type: %i[school_direct_salaried_training_programme pg_teaching_apprenticeship]) }
-
4
scope :with_study_modes, lambda { |study_modes|
-
9
where(study_mode: Array(study_modes) << "full_time_or_part_time")
-
}
-
4
scope :with_subjects, lambda { |subject_codes|
-
22
joins(:subjects).merge(Subject.with_subject_codes(subject_codes))
-
}
-
-
4
scope :with_qualifications, lambda { |qualifications|
-
9
where(qualification: qualifications)
-
}
-
-
4
scope :with_accredited_bodies, lambda { |accredited_body_codes|
-
1
where(accredited_body_code: accredited_body_codes)
-
}
-
-
4
scope :with_provider_name, lambda { |provider_name|
-
8
where(
-
provider_id: Provider.where(provider_name: provider_name),
-
).or(
-
where(
-
accredited_body_code: Provider.where(provider_name: provider_name)
-
.select(:provider_code),
-
),
-
)
-
}
-
-
4
scope :with_send, lambda {
-
3
where(is_send: true)
-
}
-
-
4
scope :with_funding_types, lambda { |funding_types|
-
23
program_types = []
-
-
23
if funding_types.include?("salary")
-
3
program_types << :school_direct_salaried_training_programme
-
end
-
-
23
if funding_types.include?("apprenticeship")
-
3
program_types << :pg_teaching_apprenticeship
-
end
-
-
23
if funding_types.include?("fee")
-
19
%i[
-
higher_education_programme
-
scitt_programme
-
school_direct_training_programme
-
].each do |program_type|
-
57
program_types << program_type
-
end
-
end
-
-
23
where(program_type: program_types)
-
}
-
-
4
scope :with_degree_grades, lambda { |degree_grades|
-
8
where(degree_grade: degree_grades)
-
}
-
-
4
scope :provider_can_sponsor_visa, lambda {
-
7
joins(:provider)
-
.where(
-
program_type: %w[school_direct_training_programme higher_education_programme scitt_programme],
-
provider: { can_sponsor_student_visa: true },
-
)
-
.or(
-
joins(:provider)
-
.where(
-
program_type: %w[school_direct_salaried_training_programme pg_teaching_apprenticeship],
-
provider: { can_sponsor_skilled_worker_visa: true },
-
),
-
)
-
}
-
-
4
def self.entry_requirement_options_without_nil_choice
-
72
ENTRY_REQUIREMENT_OPTIONS.reject { |option| option == :not_set }.keys.map(&:to_s)
-
end
-
-
4
validates :maths, inclusion: { in: entry_requirement_options_without_nil_choice }, unless: lambda {
-
2082
further_education_course? || recruitment_cycle_after_2021?
-
}
-
4
validates :english, inclusion: { in: entry_requirement_options_without_nil_choice }, unless: lambda {
-
2082
further_education_course? || recruitment_cycle_after_2021?
-
}
-
4
validates :science, inclusion: { in: entry_requirement_options_without_nil_choice }, if: lambda {
-
2082
gcse_science_required? && !recruitment_cycle_after_2021?
-
}
-
-
4
validates :is_send, inclusion: { in: [true, false] }
-
4
validates :sites, presence: true, on: %i[publish new]
-
4
validates :subjects, presence: true, on: :publish
-
4
validate :validate_enrichment_publishable, on: :publish
-
4
validate :validate_site_statuses_publishable, on: :publish
-
21
validate :validate_provider_visa_sponsorship_publishable, on: :publish, if: -> { recruitment_cycle_after_2021? }
-
21
validate :validate_provider_urn_ukprn_publishable, on: :publish, if: -> { recruitment_cycle_after_2021? }
-
4
validate :validate_degree_requirements_publishable, on: :publish
-
4
validate :validate_gcse_requirements_publishable, on: :publish
-
4
validate :validate_enrichment
-
4
validate :validate_qualification, on: %i[update new]
-
159
validate :validate_start_date, on: :update, if: -> { provider.present? && start_date.present? }
-
330
validate :validate_applications_open_from, on: %i[update new], if: -> { provider.present? }
-
4
validate :validate_modern_languages
-
4
validate :validate_has_languages, if: :has_the_modern_languages_secondary_subject_type?
-
4
validate :validate_subject_count
-
4
validate :validate_subject_consistency
-
1917
validate :validate_custom_age_range, on: %i[create new], if: -> { age_range_in_years.present? }
-
21
validate :accredited_body_exists_in_current_cycle, on: :publish, unless: -> { self_accredited? }
-
4
validates_with UniqueCourseValidator, on: :new
-
-
4
validates :name, :profpost_flag, :program_type, :qualification, :start_date, :study_mode, presence: true
-
4
validates :age_range_in_years, presence: true, on: %i[new create publish], unless: :further_education_course?
-
4
validates :level, presence: true, on: %i[new create publish]
-
-
4
def rollable_withdrawn?
-
1
content_status == :withdrawn
-
end
-
-
4
def rollable?
-
33
is_published? || rollable_withdrawn?
-
end
-
-
4
def update_notification_attributes
-
7
%w[name age_range_in_years qualification study_mode maths english science]
-
end
-
-
4
def self.get_by_codes(year, provider_code, course_code)
-
1
RecruitmentCycle.find_by(year: year)
-
.providers.find_by(provider_code: provider_code)
-
.courses.find_by(course_code: course_code)
-
end
-
-
4
def generate_name
-
170
services[:generate_course_name].execute(course: self)
-
end
-
-
4
def accrediting_provider_description
-
206
return if accrediting_provider.blank?
-
71
return if provider.accrediting_provider_enrichments.blank?
-
-
5
accrediting_provider_enrichment = provider.accrediting_provider_enrichments
-
.find do |provider|
-
5
provider.UcasProviderCode == accrediting_provider.provider_code
-
end
-
-
5
accrediting_provider_enrichment.Description if accrediting_provider_enrichment.present?
-
end
-
-
4
def publishable?
-
12
valid? :publish
-
end
-
-
4
def update_valid?
-
8
valid? :update
-
end
-
-
4
def findable?
-
982
findable_site_statuses.any?
-
end
-
-
4
def findable_site_statuses
-
988
if site_statuses.loaded?
-
460
site_statuses.select(&:findable?)
-
else
-
528
site_statuses.findable
-
end
-
end
-
-
4
def syncable_subjects
-
if subjects.loaded?
-
subjects
-
.reject { |s| s.type == "DiscontinuedSubject" }
-
.select { |s| s.subject_code.present? }
-
else
-
subjects
-
.where.not(type: "DiscontinuedSubject")
-
.where.not(subject_code: nil)
-
end
-
end
-
-
4
def open_for_applications?
-
233
applications_open_from.present? && applications_open_from <= Time.now.utc && findable?
-
end
-
-
4
def has_vacancies?
-
279
if site_statuses.loaded?
-
112
site_statuses.select(&:findable?).select(&:with_vacancies?).any?
-
else
-
167
site_statuses.findable.with_vacancies.any?
-
end
-
end
-
-
4
def has_multiple_running_sites_or_study_modes?
-
79
running_site_statuses.count > 1 || full_time_or_part_time?
-
end
-
-
4
def running_site_statuses
-
114
site_statuses.where(status: :running)
-
end
-
-
4
def update_changed_at(timestamp: Time.now.utc)
-
# Changed_at represents changes to related records as well as course
-
# itself, so we don't want to alter the semantics of updated_at which
-
# represents changes to just the course record.
-
1090
update_columns changed_at: timestamp
-
1090
touch_provider
-
end
-
-
4
def study_mode_description
-
320
study_mode.to_s.tr("_", " ")
-
end
-
-
4
def program_type_description
-
317
if school_direct_salaried_training_programme? then " with salary"
-
306
elsif pg_teaching_apprenticeship? then " teaching apprenticeship"
-
else
-
14
""
-
end
-
end
-
-
4
def description
-
317
study_mode_string = (full_time_or_part_time? ? ", " : " ") +
-
study_mode_description
-
317
qualifications_description + study_mode_string + program_type_description
-
end
-
-
4
def content_status
-
1195
services[:content_status].execute(enrichment: latest_enrichment, recruitment_cycle: recruitment_cycle)
-
end
-
-
4
def ucas_status
-
463
return :running if findable?
-
331
return :new if site_statuses.empty? || site_statuses.any?(&:status_new_status?)
-
-
25
:not_running
-
end
-
-
4
def funding_type
-
5833
return if program_type.nil?
-
-
5796
if school_direct_salaried_training_programme?
-
209
"salary"
-
5587
elsif pg_teaching_apprenticeship?
-
5105
"apprenticeship"
-
else
-
482
"fee"
-
end
-
end
-
-
4
def is_fee_based?
-
5473
funding_type == "fee"
-
end
-
-
# https://www.gov.uk/government/publications/initial-teacher-training-criteria/initial-teacher-training-itt-criteria-and-supporting-advice#c11-gcse-standard-equivalent
-
4
def gcse_subjects_required
-
2293
case level
-
when "primary"
-
1911
%w[maths english science]
-
when "secondary"
-
277
%w[maths english]
-
else
-
105
[]
-
end
-
end
-
-
4
def gcse_science_required?
-
2082
gcse_subjects_required.include?("science")
-
end
-
-
4
def gcse_grade_required
-
77
if PROVIDERS_REQUIRING_GCSE_GRADE_5.any?(provider_code) || PROVIDERS_REQUIRING_GCSE_GRADE_5.any?(accrediting_provider&.provider_code)
-
2
5
-
else
-
75
4
-
end
-
end
-
-
4
def last_published_at
-
206
latest_enrichment&.last_published_timestamp_utc
-
end
-
-
4
def publish_sites
-
5
site_statuses.status_new_status.each(&:start!)
-
5
site_statuses.status_running.unpublished_on_ucas.each(&:published_on_ucas!)
-
end
-
-
4
def publish_enrichment(current_user)
-
9
enrichments.draft.each do |enrichment|
-
9
enrichment.publish(current_user)
-
end
-
end
-
-
4
def sites=(desired_sites)
-
82
existing_sites = sites
-
-
82
if persisted?
-
17
to_add = desired_sites - existing_sites
-
38
to_add.each { |site| add_site!(site: site) }
-
-
17
to_remove = existing_sites - desired_sites
-
28
to_remove.each { |site| remove_site!(site: site) }
-
-
17
sites.reload
-
else
-
65
super(desired_sites)
-
end
-
end
-
-
4
def has_bursary?
-
113
bursary_amount.present?
-
end
-
-
4
def has_scholarship_and_bursary?
-
1
has_scholarship? && has_bursary?
-
end
-
-
4
def has_scholarship?
-
110
scholarship_amount.present?
-
end
-
-
4
def has_early_career_payments?
-
103
financial_incentive&.early_career_payments.present?
-
end
-
-
4
def bursary_amount
-
219
financial_incentive&.bursary_amount
-
end
-
-
4
def scholarship_amount
-
216
financial_incentive&.scholarship
-
end
-
-
4
def financial_incentive
-
# Ignore "modern languages" as financial incentives
-
# differ based on the language selected
-
-
1098
subjects.reject { |subject| subject.subject_name == "Modern Languages" }.first&.financial_incentive
-
end
-
-
4
def is_further_education?
-
282
level == "further_education"
-
end
-
-
4
def degree_section_complete?
-
2
degree_grade.present?
-
end
-
-
4
def is_primary?
-
35
level == "primary"
-
end
-
-
4
def is_uni_or_scitt?
-
279
provider.accredited_body?
-
end
-
-
4
def is_school_direct?
-
120
!(is_uni_or_scitt? || is_further_education?)
-
end
-
-
4
def self_accredited?
-
93
provider.accredited_body?
-
end
-
-
4
def to_s
-
3
"#{name} (#{provider_code}/#{course_code}) [#{recruitment_cycle}]"
-
end
-
-
4
def next_recruitment_cycle?
-
2
recruitment_cycle.year > RecruitmentCycle.current_recruitment_cycle.year
-
end
-
-
# Ideally this would just use the validation, but:
-
# https://github.com/rails/rails/issues/13971
-
4
def course_params_assignable(course_params, is_admin)
-
11
assignable_after_publish(course_params, is_admin) &&
-
entry_requirements_assignable(course_params) &&
-
qualification_assignable(course_params)
-
end
-
-
4
def only_published?
-
49
content_status == :published
-
end
-
-
4
def is_published?
-
689
%i[published published_with_unpublished_changes].include? content_status
-
end
-
-
4
def has_unpublished_changes?
-
69
content_status == :published_with_unpublished_changes
-
end
-
-
4
def is_running?
-
86
ucas_status == :running
-
end
-
-
4
def is_withdrawn?
-
79
content_status == "withdrawn" || not_running?
-
end
-
-
4
def not_running?
-
79
ucas_status == :not_running
-
end
-
-
4
def new_and_not_running?
-
62
ucas_status == :new
-
end
-
-
4
def funding_type=(funding_type)
-
89
assign_program_type_service = Courses::AssignProgramTypeService.new
-
89
assign_program_type_service.execute(funding_type, self)
-
end
-
-
4
def ensure_site_statuses_match_study_mode
-
site_statuses.not_no_vacancies.each do |site_status|
-
update_vac_status(study_mode, site_status)
-
end
-
end
-
-
4
def withdraw
-
8
if is_published?
-
5
site_statuses.each do |site_status|
-
13
site_status.update(vac_status: :no_vacancies, status: :suspended)
-
end
-
-
5
withdraw_latest_enrichment
-
else
-
3
errors.add(:withdraw, "Courses that have not been published should be deleted not withdrawn")
-
end
-
end
-
-
4
def assignable_master_subjects
-
186
services[:assignable_master_subjects].execute(course: self)
-
end
-
-
4
def assignable_subjects
-
1
services[:assignable_subjects].execute(course: self)
-
end
-
-
4
def in_current_cycle?
-
37
recruitment_cycle.current?
-
end
-
-
4
def age_minimum
-
103
return if age_range_in_years.blank?
-
-
102
age_range_in_years.split("_").first.to_i
-
end
-
-
4
def age_maximum
-
103
return if age_range_in_years.blank?
-
-
102
age_range_in_years.split("_").last.to_i
-
end
-
-
4
def bursary_requirements
-
104
return [] unless has_bursary?
-
-
4
requirements = [I18n.t("course.values.bursary_requirements.second_degree")]
-
4
mathematics_requirement = I18n.t("course.values.bursary_requirements.maths")
-
-
8
if subjects.any? { |subject| subject.subject_name == "Primary with mathematics" }
-
requirements.push(mathematics_requirement)
-
end
-
-
4
requirements
-
end
-
-
4
def validate_degree_requirements_publishable
-
19
return true if recruitment_cycle.year.to_i < STRUCTURED_REQUIREMENTS_REQUIRED_FROM || degree_grade.present?
-
-
1
errors.add(:base, :degree_requirements_not_publishable)
-
1
false
-
end
-
-
4
def validate_gcse_requirements_publishable
-
19
return true if recruitment_cycle.year.to_i < STRUCTURED_REQUIREMENTS_REQUIRED_FROM || !accept_pending_gcse.nil? || !accept_gcse_equivalency.nil?
-
-
14
errors.add(:base, :gcse_requirements_not_publishable)
-
14
false
-
end
-
-
4
def required_qualifications
-
101
RequiredQualificationsSummary.new(self).extract
-
end
-
-
4
def enrichment_attribute(enrichment_name)
-
235
enrichments.most_recent&.first&.public_send(enrichment_name)
-
end
-
-
4
def remove_carat_from_error_messages
-
142
new_errors = errors.map do |error|
-
945
message = error.message.start_with?("^") ? error.message[1..] : error.message
-
945
[error.attribute, message]
-
end
-
-
142
errors.clear
-
-
142
new_errors.each do |attribute, message|
-
945
errors.add attribute, message: message
-
end
-
end
-
-
4
private
-
-
4
def add_site!(site:)
-
21
is_course_new = ucas_status == :new
-
21
site_status = site_statuses.find_or_initialize_by(site: site)
-
21
site_status.start! unless is_course_new
-
21
site_status.save!
-
end
-
-
4
def remove_site!(site:)
-
11
site_status = site_statuses.find_by!(site: site)
-
11
ucas_status == :new ? site_status.destroy! : site_status.suspend!
-
end
-
-
4
def withdraw_latest_enrichment
-
5
latest_enrichment.withdraw
-
end
-
-
4
def latest_enrichment
-
1406
return if enrichments.empty?
-
-
525
if enrichments.last.created_at.nil?
-
20
enrichments.last
-
else
-
505
enrichments.max_by(&:created_at)
-
end
-
end
-
-
4
def assignable_after_publish(course_params, is_admin)
-
11
params_keys = [*(:is_send unless is_admin), :applications_open_from, :application_start_date]
-
11
relevant_params = course_params.slice(*params_keys)
-
-
11
return true if relevant_params.empty? || !is_published?
-
-
3
relevant_params.each do |field, _value|
-
3
errors.add(field.to_sym, "cannot be changed after publish")
-
end
-
3
false
-
end
-
-
4
def entry_requirements_assignable(course_params)
-
8
relevant_params = course_params.slice(:maths, :english, :science)
-
-
8
invalid_params = relevant_params.select do |_subject, value|
-
2
value && !ENTRY_REQUIREMENT_OPTIONS.key?(value.to_sym)
-
end
-
8
invalid_params.each do |subject, _value|
-
1
errors.add(subject.to_sym, "is invalid")
-
end
-
-
8
invalid_params.empty?
-
end
-
-
4
def qualification_assignable(course_params)
-
7
assignable = course_params[:qualification].nil? || Course.qualifications.include?(course_params[:qualification].to_sym)
-
7
errors.add(:qualification, "is invalid") unless assignable
-
-
7
assignable
-
end
-
-
4
def add_enrichment_errors(enrichment)
-
142
enrichment.errors.messages.map do |field, _error|
-
# `full_messages_for` here will remove any `^`s defined in the validator or en.yml.
-
# We still need it for later, so re-add it.
-
# jsonapi_errors will throw if it's given an array, so we call `.first`.
-
34
message = "^#{enrichment.errors.full_messages_for(field).first}"
-
34
errors.add field.to_sym, message
-
end
-
end
-
-
4
def validate_enrichment
-
2082
latest_enrichment = enrichments.select(&:draft?).last
-
2082
return if latest_enrichment.blank?
-
-
126
latest_enrichment.valid?
-
126
add_enrichment_errors(latest_enrichment)
-
end
-
-
4
def validate_enrichment_publishable
-
17
if enrichments.blank?
-
8
temp_enrichment = CourseEnrichment.new(course: self, status: "draft")
-
8
temp_enrichment.valid?(:publish)
-
8
add_enrichment_errors(temp_enrichment)
-
else
-
9
latest_enrichment = enrichments.select(&:draft?).last
-
-
9
if latest_enrichment
-
8
latest_enrichment.valid?(:publish)
-
8
add_enrichment_errors(latest_enrichment)
-
end
-
end
-
end
-
-
4
def validate_site_statuses_publishable
-
17
site_statuses.each do |site_status|
-
8
unless site_status.valid?
-
raise "Site status invalid on course #{provider_code}/#{course_code}: #{site_status.errors.full_messages.first}"
-
end
-
end
-
end
-
-
4
def validate_provider_visa_sponsorship_publishable
-
17
if provider.can_sponsor_student_visa.nil? || provider.can_sponsor_skilled_worker_visa.nil?
-
errors.add(:base, :visa_sponsorship_not_publishable)
-
end
-
end
-
-
4
def validate_provider_urn_ukprn_publishable
-
17
if provider.lead_school? && (provider.ukprn.blank? || provider.urn.blank?)
-
errors.add(:base, :provider_ukprn_and_urn_not_publishable)
-
17
elsif provider.ukprn.blank?
-
errors.add(:base, :provider_ukprn_not_publishable)
-
end
-
end
-
-
4
def set_defaults
-
3975
self.modular ||= ""
-
end
-
-
4
def remove_unnecessary_enrichments_validation_message
-
2082
errors.delete :enrichments if errors[:enrichments] == ["is invalid"]
-
end
-
-
4
def validate_qualification
-
326
if qualification.blank?
-
107
errors.add(:qualification, :blank)
-
else
-
219
errors.add(:qualification, "^#{qualifications_description} is not valid for a #{level.to_s.humanize.downcase} course") unless qualification.in?(qualification_options)
-
end
-
end
-
-
4
def set_applications_open_from
-
1631
self.applications_open_from ||= recruitment_cycle.application_start_date
-
end
-
-
4
def validate_start_date
-
144
errors.add :start_date, "#{start_date.strftime('%B %Y')} is not in the #{recruitment_cycle.year} cycle" unless start_date_options.include?(written_month_year(start_date))
-
end
-
-
4
def validate_applications_open_from
-
324
if applications_open_from.blank? || applications_open_from.is_a?(Struct)
-
141
errors.add(:applications_open_from, :blank)
-
183
elsif !valid_date_range.include?(applications_open_from)
-
7
chosen_date = short_date(applications_open_from)
-
7
start_date = short_date(recruitment_cycle.application_start_date)
-
7
end_date = short_date(recruitment_cycle.application_end_date)
-
7
errors.add(
-
:applications_open_from,
-
"#{chosen_date} is not valid for the #{provider.recruitment_cycle.year} cycle. " \
-
"A valid date must be between #{start_date} and #{end_date}",
-
)
-
end
-
end
-
-
4
def validate_modern_languages
-
2082
if has_any_modern_language_subject_type? && !has_the_modern_languages_secondary_subject_type?
-
errors.add(:subjects, "Modern languages subjects must also have the modern_languages subject")
-
end
-
end
-
-
4
def validate_site_status_findable
-
unless findable?
-
errors.add(:site_statuses, "must be findable")
-
end
-
end
-
-
4
def has_any_modern_language_subject_type?
-
4172
subjects.any? { |subject| subject.type == "ModernLanguagesSubject" }
-
end
-
-
4
def has_the_modern_languages_secondary_subject_type?
-
2095
raise "SecondarySubject not found" if SecondarySubject.nil?
-
2095
raise "SecondarySubject.modern_languages not found" if SecondarySubject.modern_languages.nil?
-
-
4125
subjects.any? { |subject| subject&.id == SecondarySubject.modern_languages.id }
-
end
-
-
4
def validate_has_languages
-
24
unless has_any_modern_language_subject_type?
-
11
errors.add(:modern_languages_subjects, :select_a_language)
-
end
-
end
-
-
4
def validate_subject_count
-
2082
if subjects.empty?
-
100
errors.add(:subjects, :course_creation)
-
100
return
-
end
-
-
1982
case level
-
when "primary", "further_education"
-
1713
if subjects.count > 1
-
2
errors.add(:subjects, "has too many subjects")
-
end
-
when "secondary"
-
262
if subjects.count > 2 && !has_any_modern_language_subject_type?
-
1
errors.add(:subjects, "has too many subjects")
-
end
-
end
-
end
-
-
4
def validate_subject_consistency
-
2082
subjects_excluding_discontinued = subjects.reject do |subject|
-
2033
DiscontinuedSubject.exists?(id: subject.id)
-
end
-
-
2082
return if subjects_excluding_discontinued.empty?
-
-
1982
case level
-
when "primary"
-
1691
unless PrimarySubject.exists?(id: subjects_excluding_discontinued.map(&:id))
-
1
errors.add(:subjects, "must be primary")
-
end
-
when "secondary"
-
262
unless SecondarySubject.exists?(id: subjects_excluding_discontinued.map(&:id))
-
15
errors.add(:subjects, "must be secondary")
-
end
-
when "further_education"
-
22
unless FurtherEducationSubject.exists?(id: subjects_excluding_discontinued.map(&:id))
-
1
errors.add(:subjects, "must be further education")
-
end
-
end
-
end
-
-
4
def validate_custom_age_range
-
1837
Courses::ValidateCustomAgeRangeService.new.execute(age_range_in_years, self)
-
end
-
-
4
def valid_date_range
-
183
recruitment_cycle.application_start_date..recruitment_cycle.application_end_date
-
end
-
-
4
def services
-
1552
return @services if @services.present?
-
-
579
@services = Dry::Container.new
-
579
@services.register(:generate_course_name) do
-
170
Courses::GenerateCourseNameService.new
-
end
-
579
@services.register(:assignable_master_subjects) do
-
186
Courses::AssignableMasterSubjectService.new
-
end
-
579
@services.register(:assignable_subjects) do
-
1
Courses::AssignableSubjectService.new
-
end
-
579
@services.register(:content_status) do
-
1195
Courses::ContentStatusService.new
-
end
-
end
-
-
4
def accredited_body_exists_in_current_cycle
-
16
return unless accredited_body_code
-
-
errors.add(:base, "The Accredited Body #{accredited_body_code} does not exist in this cycle") unless RecruitmentCycle.current.providers.find_by(provider_code: accredited_body_code)
-
end
-
end
-
4
class CourseEnrichment < ApplicationRecord
-
4
include TouchCourse
-
4
enum status: { draft: 0, published: 1, rolled_over: 2, withdrawn: 3 }
-
-
4
jsonb_accessor :json_data,
-
about_course: [:string, { store_key: "AboutCourse" }],
-
course_length: [:string, { store_key: "CourseLength" }],
-
fee_details: [:string, { store_key: "FeeDetails" }],
-
fee_international: [:integer, { store_key: "FeeInternational" }],
-
fee_uk_eu: [:integer, { store_key: "FeeUkEu" }],
-
financial_support: [:string, { store_key: "FinancialSupport" }],
-
how_school_placements_work: [:string,
-
{ store_key: "HowSchoolPlacementsWork" }],
-
interview_process: [:string, { store_key: "InterviewProcess" }],
-
other_requirements: [:string, { store_key: "OtherRequirements" }],
-
personal_qualities: [:string, { store_key: "PersonalQualities" }],
-
required_qualifications: [:string, { store_key: "Qualifications" }],
-
salary_details: [:string, { store_key: "SalaryDetails" }]
-
-
4
belongs_to :course
-
-
299
scope :most_recent, -> { order(created_at: :desc, id: :desc) }
-
13
scope :draft, -> { where(status: "draft").or(rolled_over) }
-
-
4
def draft?
-
488
status.in? %w[draft rolled_over]
-
end
-
-
# About this course
-
# TODO POST MIGRATION: Move out of this as it's handled in the form object
-
4
validates :about_course, presence: true, on: :publish
-
4
validates :about_course, words_count: { maximum: 400 }
-
-
4
validates :interview_process, words_count: { maximum: 250 }
-
-
4
validates :how_school_placements_work, presence: true, on: :publish
-
4
validates :how_school_placements_work, words_count: { maximum: 350 }
-
-
# Course length and fees
-
-
# TODO: POST MIGRATION: Move out of this as it's handled in the form object
-
4
validates :course_length, presence: true, on: :publish
-
-
4
validates :fee_uk_eu, presence: true, on: :publish, if: :is_fee_based?
-
4
validates :fee_uk_eu,
-
numericality: { allow_blank: true,
-
only_integer: true,
-
greater_than_or_equal_to: 0,
-
less_than_or_equal_to: 100000 },
-
if: :is_fee_based?
-
-
4
validates :fee_international,
-
numericality: { allow_blank: true,
-
only_integer: true,
-
greater_than_or_equal_to: 0,
-
less_than_or_equal_to: 100000 },
-
if: :is_fee_based?
-
-
4
validates :fee_details, words_count: { maximum: 250 }, if: :is_fee_based?
-
-
4
validates :financial_support,
-
words_count: { maximum: 250 },
-
if: :is_fee_based?
-
-
# Course length and salary
-
# TODO: POST MIGRATION: Move out of this as it's handled in the form object
-
4
validates :salary_details, presence: true, on: :publish, unless: :is_fee_based?
-
4
validates :salary_details, words_count: { maximum: 250 }, unless: :is_fee_based?
-
-
# Requirements and qualifications
-
-
4
validates :required_qualifications, presence: true, on: :publish, if: :required_qualifications_needed?
-
4
validates :required_qualifications, words_count: { maximum: 100 }
-
-
4
validates :personal_qualities, words_count: { maximum: 100 }
-
-
4
validates :other_requirements, words_count: { maximum: 100 }
-
-
4
def is_fee_based?
-
5473
course&.is_fee_based?
-
end
-
-
4
def has_been_published_before?
-
121
last_published_timestamp_utc.present?
-
end
-
-
4
def publish(current_user)
-
17
update(status: "published",
-
last_published_timestamp_utc: Time.now.utc,
-
updated_by_user_id: current_user.id)
-
end
-
-
4
def unpublish(initial_draft: true)
-
4
data = { status: :draft }
-
4
data[:last_published_timestamp_utc] = nil if initial_draft
-
4
update(data)
-
end
-
-
4
def withdraw
-
6
update(status: "withdrawn")
-
end
-
-
4
def required_qualifications_needed?
-
44
(course&.provider&.recruitment_cycle&.year.to_i) < Course::STRUCTURED_REQUIREMENTS_REQUIRED_FROM
-
end
-
end
-
4
class CourseSubject < ApplicationRecord
-
4
self.table_name = "course_subject"
-
-
4
belongs_to :course
-
4
belongs_to :subject
-
4
validates :subject_id, uniqueness: { scope: :course_id }
-
-
4
audited associated_with: :course
-
end
-
4
class FinancialIncentive < ApplicationRecord
-
4
belongs_to :subject
-
end
-
3
class InterruptPageAcknowledgement < ApplicationRecord
-
3
belongs_to :user
-
3
belongs_to :recruitment_cycle
-
-
3
ALL_PAGES = %w[rollover rollover_recruitment].freeze
-
-
3
enum page: ALL_PAGES.zip(ALL_PAGES).to_h
-
end
-
4
class Organisation < ApplicationRecord
-
4
has_many :organisation_users
-
-
4
has_many :users, through: :organisation_users
-
-
4
has_and_belongs_to_many :providers
-
-
4
validates :name, presence: true
-
-
4
has_associated_audits
-
4
audited
-
-
4
def add_user(user)
-
1
users << user
-
end
-
end
-
4
class OrganisationUser < ApplicationRecord
-
4
belongs_to :organisation
-
4
belongs_to :user
-
-
4
audited associated_with: :organisation
-
end
-
# Unpleasant hack to stop autoload error on CI
-
4
require_relative "../services/providers/generate_course_code_service"
-
-
4
class Provider < ApplicationRecord
-
4
include RegionCode
-
4
include ChangedAt
-
4
include Discard::Model
-
4
include PgSearch::Model
-
-
4
CHANGES_INTRODUCED_IN_2022_CYCLE = 2022
-
-
4
before_create :set_defaults
-
-
4
has_associated_audits
-
4
audited except: :changed_at
-
-
# NOTE: `provider_type` & `accrediting_provider`
-
# The `unknown` & `invalid_value` provider types can be removed
-
# Once the underlying the data discreptives has been amended.
-
# Validation can be added to enforce compliance.
-
# The `scitt` & `university` provider types can be used to denote
-
# that they are an `accredited_body` for accrediting providers
-
# therefore `lead_school` is a `not_an_accredited_body`.
-
4
enum provider_type: {
-
scitt: "B",
-
lead_school: "Y",
-
university: "O",
-
}
-
-
4
enum accrediting_provider: {
-
accredited_body: "Y",
-
not_an_accredited_body: "N",
-
}
-
-
4
belongs_to :recruitment_cycle
-
-
4
has_and_belongs_to_many :organisations, join_table: :organisation_provider
-
-
4
has_many :users_via_organisation, -> { kept }, through: :organisations, source: :users
-
-
4
has_many :user_permissions
-
173
has_many :users, -> { kept }, through: :user_permissions
-
-
360
has_many :sites, -> { kept }, inverse_of: :provider
-
-
4
has_many :user_notifications,
-
foreign_key: :provider_code,
-
primary_key: :provider_code,
-
inverse_of: :provider
-
-
1800
has_many :courses, -> { kept }, inverse_of: false
-
4
has_one :ucas_preferences, class_name: "ProviderUCASPreference"
-
4
has_many :contacts
-
4
has_many :accredited_courses, # use current_accredited_courses to filter to courses in the same cycle as this provider
-
50
-> { where(discarded_at: nil) },
-
class_name: "Course",
-
foreign_key: :accredited_body_code,
-
primary_key: :provider_code,
-
inverse_of: :accrediting_provider
-
-
# We have a has_many relationship rather than has_one as
-
# there are lead_schools that have PE courses ratified by more than
-
# one Accredited Body
-
4
has_many :allocations
-
-
# the accredited_providers that this provider is a training_provider for
-
106
has_many :accrediting_providers, -> { distinct }, through: :courses
-
-
4
delegate :year, to: :recruitment_cycle, prefix: true
-
-
4
def rollable_courses?
-
13
courses.any?(&:rollable?)
-
end
-
-
4
def rollable_accredited_courses?
-
accredited_courses.any?(&:rollable?)
-
end
-
-
4
def rollable?
-
13
rollable_courses? || rollable_accredited_courses?
-
end
-
-
4
def rolled_over?
-
292
FeatureService.enabled?("rollover.can_edit_current_and_next_cycles")
-
end
-
-
# the providers that this provider is an accredited_provider for
-
4
def training_providers
-
51
Provider.where(id: current_accredited_courses.pluck(:provider_id))
-
end
-
-
4
def current_accredited_courses
-
52
accredited_courses.includes(:provider).where(provider: { recruitment_cycle: recruitment_cycle })
-
end
-
-
4
scope :changed_since, lambda { |timestamp|
-
5
if timestamp.present?
-
5
where("provider.changed_at > ?", timestamp)
-
else
-
where("changed_at is not null")
-
end.order(:changed_at, :id)
-
}
-
-
48
scope :by_name_ascending, -> { order(provider_name: :asc) }
-
10
scope :by_name_descending, -> { order(provider_name: :desc) }
-
-
4
scope :by_provider_name, lambda { |provider_name|
-
2
order(
-
Arel.sql(
-
"CASE WHEN provider.provider_name = #{connection.quote(provider_name)} THEN '1' END",
-
),
-
)
-
}
-
-
4
scope :with_findable_courses, lambda {
-
11
where(id: Course.findable.select(:provider_id))
-
.or(where(provider_code: Course.findable.select(:accredited_body_code)))
-
}
-
-
609
scope :in_current_cycle, -> { where(recruitment_cycle: RecruitmentCycle.current_recruitment_cycle) }
-
-
4
scope :with_allocations_for_current_cycle_year, -> { joins(:allocations).merge(Allocation.current_allocations).order(:provider_name) }
-
-
4
scope :not_geocoded, -> { where(latitude: nil, longitude: nil).or where(region_code: nil) }
-
-
4
serialize :accrediting_provider_enrichments, AccreditingProviderEnrichment::ArraySerializer
-
-
4
validates :train_with_us, words_count: { maximum: 250, message: "^Reduce the word count for training with you" }
-
4
validates :train_with_disability, words_count: { maximum: 250, message: "^Reduce the word count for training with disabilities and other needs" }
-
-
4
validates :email, email_address: true, if: :email_changed?
-
-
4
validates :provider_name, length: { maximum: 100 }, on: :update
-
-
4
validates :provider_code, uniqueness: { scope: :recruitment_cycle }
-
-
4
validates :provider_type, presence: true
-
-
4
validates :telephone, phone: { message: "^Enter a valid telephone number" }, if: :telephone_changed?
-
-
# TODO: Remove this validation once the 2021 recruitment cycle is over
-
4
validates :ukprn, reference_number_format: { allow_blank: true, minimum: 8, maximum: 8, message: "^UKPRN must be 8 numbers" }
-
-
367
validates :ukprn, reference_number_format: { allow_blank: false, minimum: 8, maximum: 8, message: "^UKPRN must be 8 numbers" }, if: -> { recruitment_cycle.after_2021? }, on: :update
-
-
# TODO: Remove this validation once the 2021 recruitment cycle is over
-
4
validates :urn, reference_number_format: { allow_blank: true, minimum: 5, maximum: 6, message: "^URN must be 5 or 6 numbers" }, if: :lead_school?
-
-
367
validates :urn, reference_number_format: { allow_blank: false, minimum: 5, maximum: 6, message: "^URN must be 5 or 6 numbers" }, if: -> { lead_school? && recruitment_cycle.after_2021? }, on: :update
-
-
4
validates :train_with_us, presence: true, on: :update, if: :train_with_us_changed?
-
4
validates :train_with_disability, presence: true, on: :update, if: :train_with_disability_changed?
-
-
4
validate :add_enrichment_errors
-
-
4
acts_as_mappable lat_column_name: :latitude, lng_column_name: :longitude
-
-
4
before_discard do
-
4
discard_courses
-
4
discard_sites
-
end
-
-
4
pg_search_scope :provider_search,
-
against: %i[provider_code provider_name],
-
using: { tsearch: { prefix: true } }
-
-
4
pg_search_scope :course_search,
-
associated_against: {
-
courses: %i[course_code],
-
}, using: { tsearch: { prefix: true } }
-
-
4
accepts_nested_attributes_for :sites
-
4
accepts_nested_attributes_for :organisations
-
-
4
attr_accessor :skip_geocoding
-
4
after_commit :geocode_provider, unless: :skip_geocoding
-
-
4
def geocode_provider
-
2974
GeocodeJob.perform_later("Provider", id) if needs_geolocation?
-
end
-
-
4
def needs_geolocation?
-
2979
full_address.present? && (
-
2979
latitude.nil? || longitude.nil? || address_changed?
-
)
-
end
-
-
4
def full_address
-
2984
address = [provider_name, address1, address2, address3, address4, postcode]
-
-
2984
return "" if address.all?(&:blank?)
-
-
2983
address.compact.join(", ")
-
end
-
-
4
def full_address_with_breaks
-
36
[address1, address2, address3, address4, postcode].map { |line| ERB::Util.html_escape(line) }.select(&:present?).join("<br> ").html_safe
-
end
-
-
4
def address_changed?
-
14
saved_change_to_provider_name? ||
-
saved_change_to_address1? ||
-
saved_change_to_address2? ||
-
saved_change_to_address3? ||
-
saved_change_to_address4? ||
-
saved_change_to_postcode?
-
end
-
-
# Currently Provider#contact_info isn't used but will likely be needed when
-
# we need to expose the candidate-facing contact info.
-
#
-
# When the time comes:
-
# - rename this method to reflect that it's the candidate-facing contact
-
# - resurrect the tests which were stripped from models/provider_spec.rb
-
#
-
# def contact_info
-
# self
-
# .attributes_before_type_cast
-
# .slice('address1', 'address2', 'address3', 'address4', 'postcode', 'region_code', 'telephone', 'email')
-
# end
-
-
# This is used by the providers index; it is a replacement for `.includes(:courses)`,
-
# but it only fetches the counts for the associated courses. By not fetching all the
-
# course objects for 1000+ providers, the db query runs much faster, and the view spends
-
# less time rendering because there's less data to comb through.
-
4
def self.include_courses_counts
-
1
joins(
-
1
<<~EOSQL,
-
LEFT OUTER JOIN (
-
SELECT b.provider_id, COUNT(*) courses_count
-
FROM course b
-
WHERE b.discarded_at IS NULL
-
GROUP BY b.provider_id
-
) a ON a.provider_id = provider.id
-
EOSQL
-
).select("provider.*, COALESCE(a.courses_count, 0) AS included_courses_count")
-
end
-
-
4
def self.include_accredited_courses_counts(provider_code)
-
3
joins(
-
3
<<~EOSQL,
-
LEFT OUTER JOIN (
-
SELECT b.provider_id, COUNT(*) courses_count
-
FROM course b
-
WHERE b.discarded_at IS NULL
-
AND b.accredited_body_code = #{ActiveRecord::Base.connection.quote(provider_code)}
-
GROUP BY b.provider_id
-
) a ON a.provider_id = provider.id
-
EOSQL
-
).select("provider.*, COALESCE(a.courses_count, 0) AS included_accredited_courses_count")
-
end
-
-
4
def courses_count
-
2
has_attribute?("included_courses_count") ? included_courses_count : courses.size
-
end
-
-
4
def accredited_courses_count
-
3
has_attribute?("included_accredited_courses_count") ? included_accredited_courses_count : 0
-
end
-
-
4
def update_changed_at(timestamp: Time.now.utc)
-
# Changed_at represents changes to related records as well as provider
-
# itself, so we don't want to alter the semantics of updated_at which
-
# represents changes to just the provider record.
-
3666
update_columns changed_at: timestamp
-
end
-
-
# This reflects the fact that organisations should actually be a has_one.
-
4
def organisation
-
3
organisations.first
-
end
-
-
4
def provider_type=(new_value)
-
4355
super
-
4355
self.accrediting_provider = if scitt? || university?
-
474
:accredited_body
-
else
-
3881
:not_an_accredited_body
-
end
-
end
-
-
4
def to_s
-
1
"#{provider_name} (#{provider_code}) [#{recruitment_cycle}]"
-
end
-
-
4
def accredited_bodies
-
40
accrediting_providers.map do |ap|
-
35
accrediting_provider_enrichment = accrediting_provider_enrichment(ap.provider_code)
-
{
-
35
provider_name: ap.provider_name,
-
provider_code: ap.provider_code,
-
description: accrediting_provider_enrichment&.Description || "",
-
}
-
end
-
end
-
-
4
def next_available_course_code
-
6
services[:generate_unique_course_code].execute(
-
existing_codes: courses.order(:course_code).pluck(:course_code),
-
)
-
end
-
-
4
def discard_courses
-
5
courses.each(&:discard)
-
end
-
-
4
def discard_sites
-
5
sites.each(&:discard)
-
end
-
-
4
def declared_visa_sponsorship?
-
22
!can_sponsor_student_visa.nil? && !can_sponsor_skilled_worker_visa.nil?
-
end
-
-
4
def can_sponsor_all_visas?
-
8
can_sponsor_student_visa && can_sponsor_skilled_worker_visa
-
end
-
-
4
def can_only_sponsor_student_visa?
-
7
can_sponsor_student_visa && !can_sponsor_skilled_worker_visa
-
end
-
-
4
def can_only_sponsor_skilled_worker_visa?
-
3
!can_sponsor_student_visa && can_sponsor_skilled_worker_visa
-
end
-
-
4
def cannot_sponsor_visas?
-
2
can_sponsor_student_visa == false && can_sponsor_skilled_worker_visa == false
-
end
-
-
4
def from_next_recruitment_cycle
-
1
Provider.joins(:recruitment_cycle).where(recruitment_cycle: { year: Settings.current_recruitment_cycle_year.succ.to_s }).find_by(provider_code: provider_code)
-
end
-
-
4
private
-
-
4
scope :course_code_search, ->(course_code) { joins(:courses).merge(Course.case_insensitive_search(course_code)) }
-
-
4
def accrediting_provider_enrichment(provider_code)
-
35
accrediting_provider_enrichments&.find do |enrichment|
-
18
enrichment.UcasProviderCode == provider_code
-
end
-
end
-
-
4
def add_enrichment_errors
-
3381
accrediting_provider_enrichments&.each do |item|
-
179
accrediting_provider = accrediting_providers.find { |ap| ap.provider_code == item.UcasProviderCode }
-
-
65
if accrediting_provider.present? && item.invalid?
-
10
message = "^Reduce the word count for #{accrediting_provider.provider_name}"
-
10
errors.add :accredited_bodies, message
-
end
-
end
-
end
-
-
4
def set_defaults
-
2943
self.year_code ||= recruitment_cycle.year
-
end
-
-
4
def services
-
6
return @services if @services.present?
-
-
6
@services = Dry::Container.new
-
6
@services.register(:generate_unique_course_code) do
-
6
Providers::GenerateUniqueCourseCodeService.new(
-
generate_course_code_service: Providers::GenerateCourseCodeService.new,
-
)
-
end
-
end
-
end
-
4
class ProviderUCASPreference < ApplicationRecord
-
4
belongs_to :provider
-
-
4
enum type_of_gt12: {
-
coming_or_not: "Coming or Not",
-
coming_enrol: "Coming / Enrol",
-
not_coming: "Not coming",
-
no_response: "No response",
-
},
-
_prefix: "type_of_gt12"
-
-
4
enum send_application_alerts: {
-
all: "Yes, required",
-
none: "No, not required",
-
my_programmes: "Yes - only my programmes",
-
accredited_programmes: "Yes - for accredited programmes only",
-
},
-
_prefix: "send_application_alerts_for"
-
-
4
def gt12_contact=(gt12_contact)
-
1
update(gt12_response_destination: gt12_contact)
-
end
-
-
4
def application_alert_contact=(application_alert_contact)
-
2
update(application_alert_email: application_alert_contact)
-
end
-
end
-
4
class RecruitmentCycle < ApplicationRecord
-
2348
has_many :providers, -> { kept }, inverse_of: false
-
# Because this is through a has_many, these associations can't be updated,
-
# which is a good thing since we don't have a good way to "move" a course or
-
# a site to a new recruitment_cycle
-
4
has_many :courses, through: :providers
-
4
has_many :sites, through: :providers
-
4
has_many :allocations
-
-
4
validates :year, presence: true
-
-
4
class << self
-
4
def current_recruitment_cycle
-
2761
find_by(year: Settings.current_recruitment_cycle_year)
-
end
-
4
alias_method :current, :current_recruitment_cycle
-
-
4
def next_recruitment_cycle
-
867
current_recruitment_cycle.next
-
end
-
4
alias_method :next, :next_recruitment_cycle
-
end
-
-
4
def previous
-
50
RecruitmentCycle.find_by(year: year.to_i - 1)
-
end
-
-
4
def next
-
926
RecruitmentCycle.find_by(year: year.to_i + 1)
-
end
-
-
4
def next?
-
823
RecruitmentCycle.next_recruitment_cycle == self
-
end
-
-
4
def current?
-
298
RecruitmentCycle.current_recruitment_cycle == self
-
end
-
-
4
def current_and_open?
-
106
current? && FeatureService.enabled?("rollover.has_current_cycle_started?")
-
end
-
-
4
def to_s
-
7
following_year = Date.new(year.to_i, 1, 1) + 1.year
-
7
"#{year}/#{following_year.strftime('%y')}"
-
end
-
-
4
def title
-
if current_and_open?
-
"Current cycle (#{year_range})"
-
elsif current?
-
"New cycle (#{year_range})"
-
elsif next?
-
"Next cycle (#{year_range})"
-
else
-
year_range
-
end
-
end
-
-
4
def year_range
-
"#{year} to #{year.to_i + 1}"
-
end
-
-
# TODO: remove once the 2022 rollover is complete
-
4
def after_2021?
-
6548
year.to_i >= 2022
-
end
-
end
-
3
class RequiredQualificationsSummary
-
3
attr_reader :course
-
-
3
def initialize(course)
-
120
@course = course
-
end
-
-
3
def extract
-
120
legacy_qualifications_attribute = course.latest_published_enrichment&.required_qualifications
-
120
return legacy_qualifications_attribute if legacy_qualifications_attribute.present?
-
-
59
generate_summary_text
-
end
-
-
3
private
-
-
3
def generate_summary_text
-
59
output = ""
-
59
output << required_gcse_content << "\n"
-
59
output << pending_gcse_content << "\n"
-
59
output << gcse_equivalency_content << "\n"
-
59
output << (course.additional_gcse_equivalencies || "") << "\n"
-
59
output << degree_grade_content << "\n"
-
59
output << degree_subject_requirements_content
-
-
59
output.strip
-
end
-
-
3
def required_gcse_content
-
59
case course.level
-
when "primary"
-
57
"Grade #{course.gcse_grade_required} (C) or above in English, maths and science, or equivalent qualification."
-
when "secondary"
-
1
"Grade #{course.gcse_grade_required} (C) or above in English and maths, or equivalent qualification."
-
else
-
1
""
-
end
-
end
-
-
3
def pending_gcse_content
-
59
if course.accept_pending_gcse?
-
1
"We will consider candidates with pending GCSEs."
-
else
-
58
"We will not consider candidates with pending GCSEs."
-
end
-
end
-
-
3
def gcse_equivalency_content
-
59
return "We do not accept equivalency tests." unless course.accept_gcse_equivalency?
-
-
4
case gcse_equivalencies.count
-
when 0
-
1
"" # Assume that course.additional_gcse_equivalencies is populated instead
-
when 1
-
1
"We will accept equivalency tests in #{gcse_equivalencies[0].capitalize}."
-
when 2
-
1
"We will accept equivalency tests in #{gcse_equivalencies[0].capitalize} and #{gcse_equivalencies[1]}."
-
when 3
-
1
"We will accept equivalency tests in #{gcse_equivalencies[0].capitalize}, #{gcse_equivalencies[1]} and #{gcse_equivalencies[2]}."
-
end
-
end
-
-
3
def gcse_equivalencies
-
{
-
10
english: course.accept_english_gcse_equivalency?,
-
maths: course.accept_maths_gcse_equivalency?,
-
science: course.accept_science_gcse_equivalency?,
-
30
}.select { |_k, v| v }.keys
-
end
-
-
3
def degree_grade_content
-
{
-
59
"two_one" => "An undergraduate degree at class 2:1 or above, or equivalent.",
-
"two_two" => "An undergraduate degree at class 2:2 or above, or equivalent.",
-
"third_class" => "An undergraduate degree, or equivalent. This should be an honours degree (Third or above), or equivalent.",
-
"not_required" => "An undergraduate degree, or equivalent.",
-
nil => "",
-
}[course.degree_grade]
-
end
-
-
3
def degree_subject_requirements_content
-
59
if course.additional_degree_subject_requirements?
-
59
course.degree_subject_requirements
-
else
-
""
-
end
-
end
-
end
-
4
class Site < ApplicationRecord
-
4
MAIN_SITE = "main site".freeze
-
4
URN_2022_REQUIREMENTS_REQUIRED_FROM = 2022
-
-
4
include PostcodeNormalize
-
4
include RegionCode
-
4
include TouchProvider
-
4
include Discard::Model
-
-
4
POSSIBLE_CODES = (("A".."Z").to_a + ("0".."9").to_a + ["-"]).freeze
-
4
EASILY_CONFUSED_CODES = %w[1 I 0 O -].freeze # these ought to be assigned last
-
4
DESIRABLE_CODES = (POSSIBLE_CODES - EASILY_CONFUSED_CODES).freeze
-
-
4
before_validation :assign_code, unless: :persisted?
-
-
4
audited associated_with: :provider
-
-
4
has_many :site_statuses, dependent: :destroy
-
4
belongs_to :provider
-
-
4
validates :location_name, uniqueness: { scope: :provider_id }
-
4
validates :location_name,
-
:address1,
-
:postcode,
-
presence: true
-
4
validates :postcode, postcode: true
-
835
validates :code, uniqueness: { scope: :provider_id, case_sensitive: false, conditions: -> { where(discarded_at: nil) } },
-
format: { with: /\A[A-Z0-9\-]+\z/, message: "must contain only A-Z, 0-9 or -" },
-
presence: true
-
-
4
validates :urn, reference_number_format: { allow_blank: true, minimum: 5, maximum: 6, message: "^URN must be 5 or 6 numbers" }
-
-
4
acts_as_mappable lat_column_name: :latitude, lng_column_name: :longitude
-
-
4
scope :not_geocoded, -> { where(latitude: nil, longitude: nil) }
-
-
4
attr_accessor :skip_geocoding
-
4
after_commit :geocode_site, unless: :skip_geocoding
-
-
4
delegate :recruitment_cycle, :provider_code, to: :provider, allow_nil: true
-
4
delegate :after_2021?, to: :recruitment_cycle, allow_nil: true, prefix: :recruitment_cycle
-
-
4
def geocode_site
-
730
GeocodeJob.perform_later("Site", id) if needs_geolocation?
-
end
-
-
4
def needs_geolocation?
-
735
full_address.present? && (
-
734
latitude.nil? || longitude.nil? || address_changed?
-
)
-
end
-
-
4
def full_address
-
773
address = [address1, address2, address3, address4, postcode]
-
-
773
unless location_name.downcase == MAIN_SITE
-
772
address.unshift(location_name)
-
end
-
-
773
return "" if address.all?(&:blank?)
-
-
770
address.select(&:present?).join(", ")
-
end
-
-
4
def address_changed?
-
60
saved_change_to_location_name? ||
-
saved_change_to_address1? ||
-
saved_change_to_address2? ||
-
saved_change_to_address3? ||
-
saved_change_to_address4? ||
-
saved_change_to_postcode?
-
end
-
-
4
delegate :recruitment_cycle, to: :provider
-
-
4
def assign_code
-
701
self.code ||= Sites::CodeGenerator.call(provider: provider)
-
end
-
-
4
def to_s
-
1
"#{location_name} (code: #{code})"
-
end
-
end
-
4
class SiteStatus < ApplicationRecord
-
4
include TouchCourse
-
4
include AASM
-
-
4
self.table_name = "course_site"
-
-
4
after_initialize :set_defaults
-
4
before_validation :set_vac_status
-
-
4
audited associated_with: :course
-
-
4
validate :vac_status_must_be_consistent_with_course_study_mode,
-
715
if: proc { |s| s.course&.study_mode.present? }
-
-
4
enum vac_status: {
-
both_full_time_and_part_time_vacancies: "B",
-
part_time_vacancies: "P",
-
full_time_vacancies: "F",
-
no_vacancies: "",
-
}
-
-
4
enum status: {
-
discontinued: "D",
-
running: "R",
-
new_status: "N",
-
suspended: "S",
-
}, _prefix: :status
-
-
4
enum publish: {
-
published: "Y",
-
unpublished: "N",
-
}, _suffix: :on_ucas
-
-
4
aasm column: :status, enum: true do
-
4
state :new_status, initial: true
-
4
state :running
-
4
state :suspended
-
4
state :discontinued
-
-
4
after_all_transitions :update_publish_flag
-
-
4
event :start do
-
4
transitions from: %i[new_status suspended discontinued], to: :running
-
end
-
-
4
event :suspend do
-
4
transitions from: :running, to: :suspended
-
end
-
end
-
-
4
def update_publish_flag
-
37
self.publish = (aasm.to_state == :running ? :published : :unpublished)
-
end
-
-
4
belongs_to :site
-
4
belongs_to :course
-
-
4
acts_as_mappable through: :site
-
1072
scope :findable, -> { status_running.published_on_ucas }
-
-
4
def findable?
-
579
status_running? && published_on_ucas?
-
end
-
-
217
scope :with_vacancies, -> { where.not(vac_status: :no_vacancies).findable }
-
7
scope :new_or_running, -> { where(status: %i[running new_status]) }
-
-
4
def with_vacancies?
-
105
!no_vacancies? && findable?
-
end
-
-
4
def self.default_vac_status_given(study_mode:)
-
102
case study_mode
-
when "full_time"
-
60
:full_time_vacancies
-
when "part_time"
-
35
:part_time_vacancies
-
when "full_time_or_part_time"
-
6
:both_full_time_and_part_time_vacancies
-
else
-
1
:no_vacancies
-
end
-
end
-
-
4
def has_vacancies?
-
20
with_vacancies?
-
end
-
-
4
def vacancies_filled?
-
2
will_save_change_to_attribute?(:vac_status, to: "no_vacancies") && running?
-
end
-
-
4
private
-
-
4
def set_defaults
-
874
self.status ||= :new_status
-
874
self.publish ||= :unpublished
-
end
-
-
4
def set_vac_status
-
715
self.vac_status ||= self.class.default_vac_status_given(study_mode: course.study_mode)
-
end
-
-
4
def vac_status_must_be_consistent_with_course_study_mode
-
714
unless vac_status_consistent_with_course_study_mode?
-
8
errors.add(:vac_status, "(#{vac_status}) must be consistent with course study mode #{course.study_mode}")
-
end
-
end
-
-
4
def vac_status_consistent_with_course_study_mode?
-
714
case vac_status
-
when "no_vacancies"
-
72
true
-
when "full_time_vacancies"
-
568
course.full_time? || course.full_time_or_part_time?
-
when "part_time_vacancies"
-
45
course.part_time? || course.full_time_or_part_time?
-
when "both_full_time_and_part_time_vacancies"
-
29
course.full_time_or_part_time?
-
else
-
false
-
end
-
end
-
end
-
1
class Statistic < ApplicationRecord
-
end
-
4
class Subject < ApplicationRecord
-
4
has_many :course_subjects
-
4
has_many :courses, through: :course_subjects
-
4
belongs_to :subject_area, foreign_key: :type, inverse_of: :subjects
-
4
has_one :financial_incentive
-
-
4
scope :with_subject_codes, lambda { |subject_codes|
-
22
where(subject_code: subject_codes)
-
}
-
-
38
scope :active, -> { where.not(type: "DiscontinuedSubject") }
-
-
4
def secondary_subject?
-
2720
type == "SecondarySubject"
-
end
-
-
4
def to_sym
-
1
subject_name.parameterize.underscore.to_sym
-
end
-
-
4
def to_s
-
185
subject_name
-
end
-
end
-
4
class SubjectArea < ApplicationRecord
-
4
has_many :subjects, foreign_key: :type, inverse_of: :subject_area
-
4
self.primary_key = :typename
-
13
scope :active, -> { where.not(typename: "DiscontinuedSubject") }
-
end
-
4
class DiscontinuedSubject < Subject
-
end
-
4
class FurtherEducationSubject < Subject
-
4
def self.instance
-
7
find_by(subject_name: "Further education")
-
end
-
end
-
4
class ModernLanguagesSubject < Subject
-
end
-
4
class PrimarySubject < Subject
-
end
-
4
class SecondarySubject < Subject
-
4
def self.modern_languages
-
4503
@modern_languages ||= find_by(subject_name: "Modern Languages")
-
end
-
-
4
def self.clear_modern_languages_cache
-
24
@modern_languages = nil
-
end
-
end
-
4
class User < ApplicationRecord
-
4
include Discard::Model
-
4
include PgSearch::Model
-
-
4
has_many :organisation_users
-
-
# dependent destroy because https://stackoverflow.com/questions/34073757/removing-relations-is-not-being-audited-by-audited-gem/34078860#34078860
-
4
has_many :organisations, through: :organisation_users, dependent: :destroy
-
-
4
has_many :user_notifications, class_name: "UserNotification"
-
-
4
has_many :providers_via_organisations, through: :organisations, source: :providers
-
-
4
has_many :user_permissions
-
4
has_many :providers, through: :user_permissions
-
-
4
has_many :access_requests,
-
foreign_key: :requester_id,
-
primary_key: :id,
-
inverse_of: "requester"
-
-
4
has_many :interrupt_page_acknowledgements
-
-
6
scope :admins, -> { where(admin: true) }
-
8
scope :non_admins, -> { where.not(admin: true) }
-
15
scope :active, -> { where.not(accept_terms_date_utc: nil) }
-
4
scope :last_login_since, lambda { |timestamp|
-
11
where("last_login_date_utc > ?", timestamp)
-
}
-
4
scope :course_update_subscribers, lambda { |accredited_body_code|
-
14
joins(:user_notifications).merge(UserNotification.course_update_notification_requests(accredited_body_code))
-
}
-
4
scope :course_publish_subscribers, lambda { |accredited_body_code|
-
22
joins(:user_notifications).merge(UserNotification.course_publish_notification_requests(accredited_body_code))
-
}
-
-
4
pg_search_scope :search, against: %i[first_name last_name email], using: { tsearch: { prefix: true } }
-
-
4
validates :first_name, presence: true
-
4
validates :last_name, presence: true
-
4
validates :email, presence: true, format: { with: /\A.*@.*\z/, message: "must contain @" }, uniqueness: true
-
4
validate :email_is_lowercase
-
-
4
validates :email, if: :admin?, format: {
-
with: /\A.*@(digital\.){0,1}education\.gov\.uk\z/,
-
message: "must be an @[digital.]education.gov.uk domain",
-
}
-
-
4
audited
-
-
4
def to_s
-
1
"#{first_name} #{last_name} <#{email}>"
-
end
-
-
4
def remove_access_to(providers_to_remove)
-
10
self.providers = providers - Array(providers_to_remove)
-
end
-
-
4
def associated_with_accredited_body?
-
559
providers
-
.in_current_cycle
-
.accredited_body
-
.count
-
.positive?
-
end
-
-
4
def notifications_configured?
-
2
user_notifications.count.positive?
-
end
-
-
4
def full_name
-
42
"#{first_name} #{last_name}"
-
end
-
-
4
def accepted_terms?
-
1065
accept_terms_date_utc.present?
-
end
-
-
4
def current_rollover_acceptance
-
7
current_page_acknowledgement_for("rollover")
-
end
-
-
4
def current_rollover_recruitment_acceptance
-
5
current_page_acknowledgement_for("rollover_recruitment")
-
end
-
-
4
def has_multiple_providers?
-
259
providers.count > 1
-
end
-
-
4
def has_multiple_providers_in_current_recruitment_cycle?
-
17
providers_via_organisations.where(recruitment_cycle: RecruitmentCycle.current).count > 1
-
end
-
-
4
private
-
-
4
def email_is_lowercase
-
1381
if email.present? && email.downcase != email
-
3
errors.add(:email, "must be lowercase")
-
end
-
end
-
-
4
def current_page_acknowledgement_for(page)
-
12
interrupt_page_acknowledgements
-
.includes(:recruitment_cycle).find_by(page: page, recruitment_cycle: { year: Settings.current_recruitment_cycle_year })
-
end
-
end
-
4
class UserNotification < ApplicationRecord
-
4
belongs_to :user,
-
inverse_of: :user_notifications
-
-
4
belongs_to :provider,
-
foreign_key: :provider_code,
-
primary_key: :provider_code,
-
inverse_of: :user_notifications
-
-
4
validates :course_publish, :course_update, inclusion: { in: [true, false] }
-
-
4
scope :course_publish_notification_requests, lambda { |provider_code|
-
23
where(provider_code: provider_code, course_publish: true)
-
}
-
-
4
scope :course_update_notification_requests, lambda { |provider_code|
-
19
where(provider_code: provider_code, course_update: true)
-
}
-
end
-
3
class UserNotificationPreferences
-
3
def initialize(user_id:)
-
33
@user_id = user_id
-
end
-
-
3
def id
-
93
@user_id&.to_i
-
end
-
-
3
def enabled
-
# course_publish and course_update currently always have the same value
-
16
user_notifications.select(&:course_publish).any?
-
end
-
-
3
def updated_at
-
20
return if user_notifications.empty?
-
-
9
user_notifications.maximum(:updated_at).iso8601
-
end
-
-
3
def update(enable_notifications:)
-
18
UserNotification.transaction do
-
18
rollback_on_error do
-
18
UserNotification.where(user_id: id).destroy_all
-
-
18
user_accredited_body_codes.each do |provider_code|
-
29
UserNotification.create(
-
user_id: id,
-
course_publish: enable_notifications,
-
course_update: enable_notifications,
-
provider_code: provider_code,
-
)
-
end
-
end
-
end
-
-
18
reset_user_notifications
-
-
18
self
-
end
-
-
3
alias_method :enabled?, :enabled
-
-
3
private
-
-
3
def user_accredited_body_codes
-
18
user.providers.accredited_body.in_current_cycle.distinct.pluck(:provider_code)
-
end
-
-
3
def user_notifications
-
45
@user_notifications ||= UserNotification.where(user_id: id)
-
end
-
-
3
def reset_user_notifications
-
18
@user_notifications = nil
-
end
-
-
3
def user
-
18
@user ||= User.find(id)
-
end
-
-
3
def rollback_on_error
-
18
yield
-
rescue StandardError => e
-
2
Sentry.capture_exception(e)
-
2
raise ActiveRecord::Rollback
-
end
-
end
-
4
class UserPermission < ApplicationRecord
-
4
belongs_to :user
-
4
belongs_to :provider
-
end
-
# frozen_string_literal: true
-
-
4
class UserSession
-
4
attr_reader :email, :sign_in_user_id
-
4
attr_accessor :first_name, :last_name
-
-
4
def initialize(email:, sign_in_user_id:, first_name:, last_name:, id_token: nil, provider: "dfe")
-
1262
@email = email&.downcase
-
1262
@sign_in_user_id = sign_in_user_id
-
1262
@first_name = first_name
-
1262
@last_name = last_name
-
1262
@id_token = id_token
-
1262
@provider = provider&.to_s
-
end
-
-
4
def self.begin_session!(session, omniauth_payload)
-
225
session["user"] = {
-
"email" => omniauth_payload["info"]["email"],
-
"sign_in_user_id" => omniauth_payload["uid"],
-
"first_name" => omniauth_payload["info"]["first_name"],
-
"last_name" => omniauth_payload["info"]["last_name"],
-
"last_active_at" => Time.zone.now,
-
"id_token" => omniauth_payload["credentials"]["id_token"],
-
"provider" => omniauth_payload["provider"],
-
}
-
end
-
-
4
def self.load_from_session(session)
-
2077
user_session = session["user"]
-
2077
return unless user_session
-
-
1259
return if user_session.fetch("last_active_at") < 2.hours.ago
-
-
1258
user_session["last_active_at"] = Time.zone.now
-
-
1258
new(
-
email: user_session["email"],
-
sign_in_user_id: user_session["sign_in_user_id"],
-
first_name: user_session["first_name"],
-
last_name: user_session["last_name"],
-
id_token: user_session["id_token"],
-
provider: user_session["provider"],
-
)
-
end
-
-
4
def self.end_session!(session)
-
3
session.delete("user")
-
end
-
-
4
def logout_url
-
1
if AuthenticationService.magic_link? || AuthenticationService.persona?
-
"/sign-in"
-
else
-
1
dfe_logout_url
-
end
-
end
-
-
4
private
-
-
4
def dfe_logout_url
-
1
uri = URI("#{Settings.dfe_signin.issuer}/session/end")
-
1
uri.query = {
-
id_token_hint: @id_token,
-
post_logout_redirect_uri: "#{Settings.base_url}/auth/dfe/signout",
-
}.to_query
-
1
uri.to_s
-
end
-
end
-
2
class AccessRequestPolicy
-
2
attr_reader :user, :access_request
-
-
2
def initialize(user, _access_request)
-
17
@user = user
-
end
-
-
2
def approve?
-
15
@user.admin?
-
end
-
-
2
def create?
-
2
@user.present?
-
end
-
-
2
alias_method :new?, :create?
-
2
alias_method :index?, :approve?
-
2
alias_method :show?, :approve?
-
2
alias_method :inform_publisher?, :approve?
-
2
alias_method :confirm?, :approve?
-
2
alias_method :destroy?, :approve?
-
end
-
3
class AllocationPolicy
-
3
attr_reader :user, :allocation
-
-
3
class Scope
-
3
attr_reader :user, :scope
-
-
3
def initialize(user, scope)
-
3
@user = user
-
3
@scope = scope
-
end
-
-
3
def resolve
-
3
if user.admin?
-
1
scope.all
-
else
-
2
scope
-
.where(accredited_body_id: user.providers.pluck(:id))
-
end
-
end
-
end
-
-
3
def initialize(user, allocation)
-
48
@allocation = allocation
-
48
@user = user
-
end
-
-
3
def index?
-
2
user.present?
-
end
-
-
3
def show?
-
6
user_is_admin_or_belongs_to_accredited_body?
-
end
-
-
3
def create?
-
7
user_is_admin_or_belongs_to_accredited_body?
-
end
-
-
3
def update?
-
33
user_is_admin_or_belongs_to_accredited_body?
-
end
-
-
3
def destroy?
-
user_is_admin_or_belongs_to_accredited_body?
-
end
-
-
3
alias_method :edit?, :update?
-
3
alias_method :delete?, :update?
-
3
alias_method :confirm_deletion?, :index?
-
3
alias_method :initial_request?, :index?
-
3
alias_method :new_repeat_request?, :index?
-
-
3
private
-
-
3
def user_belongs_to_the_accredited_body?
-
46
user.providers.include?(allocation.accredited_body)
-
end
-
-
3
def user_is_admin_or_belongs_to_accredited_body?
-
46
user_belongs_to_the_accredited_body? || user.admin?
-
end
-
end
-
1
class ContactPolicy
-
1
attr_reader :user, :contact
-
-
1
def initialize(user, contact)
-
6
@user = user
-
6
@contact = contact
-
end
-
-
1
def show?
-
3
user_is_admin_or_belongs_to_provider?
-
end
-
-
1
def update?
-
3
user_is_admin_or_belongs_to_provider?
-
end
-
-
1
private
-
-
1
def user_belongs_to_the_provider?
-
6
user.providers.include?(contact.provider)
-
end
-
-
1
def user_is_admin_or_belongs_to_provider?
-
6
user_belongs_to_the_provider? || user.admin?
-
end
-
end
-
4
class CoursePolicy
-
4
attr_reader :user, :course
-
-
4
class Scope
-
4
attr_reader :user, :scope
-
-
4
def initialize(user, scope)
-
4
@user = user
-
4
@scope = scope
-
end
-
-
4
def resolve
-
4
if user.admin?
-
1
scope.all
-
else
-
3
scope
-
.where(provider_id: user.providers.pluck(:id))
-
.or(Course.where(accredited_body_code: user.providers.pluck(:provider_code)))
-
end
-
end
-
end
-
-
4
def initialize(user, course)
-
606
@user = user
-
606
@course = course
-
end
-
-
4
def index?
-
2
user.present?
-
end
-
-
4
def show?
-
56
user.admin? || user.providers.include?(course.provider)
-
end
-
-
4
def send_vacancies_updated_notification?
-
user.present?
-
end
-
-
4
alias_method :preview?, :show?
-
4
alias_method :details?, :show?
-
4
alias_method :update?, :show?
-
4
alias_method :edit?, :show?
-
4
alias_method :destroy?, :show?
-
4
alias_method :publish?, :update?
-
4
alias_method :publishable?, :update?
-
4
alias_method :new?, :index?
-
4
alias_method :withdraw?, :show?
-
-
4
def permitted_attributes
-
4
if user.admin?
-
2
permitted_admin_attributes
-
else
-
2
permitted_user_attributes
-
end
-
end
-
-
4
def permitted_new_course_attributes
-
550
%i[
-
accredited_body_code
-
age_range_in_years
-
applications_open_from
-
funding_type
-
is_send
-
level
-
qualification
-
start_date
-
study_mode
-
]
-
end
-
-
4
private
-
-
4
def permitted_user_attributes
-
4
permitted_new_course_attributes + %i[
-
english
-
maths
-
science
-
degree_grade
-
additional_degree_subject_requirements
-
degree_subject_requirements
-
accept_pending_gcse
-
accept_gcse_equivalency
-
accept_english_gcse_equivalency
-
accept_maths_gcse_equivalency
-
accept_science_gcse_equivalency
-
additional_gcse_equivalencies
-
]
-
end
-
-
4
def permitted_admin_attributes
-
2
permitted_user_attributes + [:name]
-
end
-
end
-
1
class OrganisationPolicy
-
1
attr_reader :user, :accessed_organisation
-
-
1
def initialize(user, accessed_organisation)
-
2
@user = user
-
2
@accessed_organisation = accessed_organisation
-
end
-
-
1
def add_user?
-
2
@user.admin?
-
end
-
-
1
alias_method :index?, :add_user?
-
end
-
4
class ProviderPolicy
-
4
attr_reader :user, :provider
-
-
4
class Scope
-
4
attr_reader :user, :scope
-
-
4
def initialize(user, scope)
-
1
@user = user
-
1
@scope = scope
-
end
-
-
4
def resolve
-
1
if user.admin?
-
scope.all
-
else
-
1
scope.where(id: user.providers)
-
end
-
end
-
end
-
-
4
def initialize(user, provider)
-
784
@user = user
-
784
@provider = provider
-
end
-
-
4
def index?
-
259
user.present?
-
end
-
-
4
def search?
-
user.admin?
-
end
-
-
4
def show?
-
514
user.admin? || user.providers.include?(provider)
-
end
-
-
4
def show_any?
-
user.present?
-
end
-
-
4
def create?
-
3
user.admin?
-
end
-
-
4
def suggest?
-
1
user.present?
-
end
-
-
4
def suggest_any?
-
user.present?
-
end
-
-
4
def can_show_training_provider?
-
4
return true if user.admin?
-
-
4
accredited_bodies_codes = provider.accredited_bodies.map { |ab| ab[:provider_code] }
-
2
user_provider_codes = user.providers.pluck(:provider_code)
-
-
2
!(accredited_bodies_codes & user_provider_codes).compact.empty?
-
end
-
-
4
alias_method :can_list_sites?, :show?
-
4
alias_method :can_create_sites?, :show?
-
4
alias_method :can_create_course?, :show?
-
4
alias_method :edit?, :show?
-
4
alias_method :update?, :show?
-
4
alias_method :destroy?, :show?
-
4
alias_method :build_new?, :show?
-
4
alias_method :can_list_training_providers?, :show?
-
-
4
def permitted_provider_attributes
-
4
if user.admin?
-
2
admin_provider_attributes
-
else
-
2
user_provider_attributes
-
end
-
end
-
-
4
private
-
-
4
def user_provider_attributes
-
4
base_attributes = %i[
-
train_with_us
-
train_with_disability
-
email
-
telephone
-
website
-
address1
-
address2
-
address3
-
address4
-
postcode
-
region_code
-
ukprn
-
can_sponsor_skilled_worker_visa
-
can_sponsor_student_visa
-
]
-
4
provider.lead_school? ? base_attributes << :urn : base_attributes
-
end
-
-
4
def admin_provider_attributes
-
2
user_provider_attributes + [:provider_name]
-
end
-
end
-
1
class RecruitmentCyclePolicy
-
1
attr_reader :user, :recruitment_cycle
-
-
1
class Scope
-
1
attr_reader :user, :scope
-
-
1
def initialize(user, scope)
-
1
@user = user
-
1
@scope = scope
-
end
-
-
1
def resolve
-
# If a user is logged in, they can view all recruitment cycles
-
1
scope
-
end
-
end
-
-
1
def initialize(user, recycle)
-
3
@user = user
-
3
@recruitment_cycle = recycle
-
end
-
-
1
def index?
-
1
user.present?
-
end
-
-
1
def show?
-
2
user.present?
-
end
-
end
-
2
class SitePolicy
-
2
attr_reader :user, :site
-
-
2
def initialize(user, site)
-
4
@user = user
-
4
@site = site
-
end
-
-
2
def index?
-
1
user.present?
-
end
-
-
2
def show?
-
3
user.admin? || user.providers.include?(site.provider)
-
end
-
-
2
alias_method :update?, :show?
-
2
alias_method :create?, :show?
-
end
-
1
class SiteStatusPolicy
-
1
attr_reader :user, :site_status
-
-
1
def initialize(user, site_status)
-
2
@user = user
-
2
@site_status = site_status
-
end
-
-
1
def update?
-
2
CoursePolicy.new(user, site_status&.course).update?
-
end
-
end
-
1
class UserNotificationPreferencesPolicy
-
1
def initialize(user, user_notification_preferences)
-
4
@user = user
-
4
@user_notification_preferences = user_notification_preferences
-
end
-
-
1
def show?
-
2
user.present? && user_matches?
-
end
-
-
1
def update?
-
2
user.present? && user_matches?
-
end
-
-
1
private
-
-
1
attr_reader :user, :user_notification_preferences
-
-
1
def user_matches?
-
4
user.id == user_notification_preferences.id
-
end
-
end
-
1
class UserPolicy
-
1
attr_reader :user, :accessed_user
-
-
1
def initialize(user, accessed_user)
-
9
@user = user
-
9
@accessed_user = accessed_user
-
end
-
-
1
def show?
-
6
user == accessed_user
-
end
-
-
1
def remove_access_to?
-
3
user.admin? || user == accessed_user
-
end
-
-
1
alias_method :update?, :show?
-
1
alias_method :accept_transition_screen?, :update?
-
1
alias_method :accept_rollover_screen?, :update?
-
1
alias_method :accept_terms?, :update?
-
1
alias_method :index?, :show?
-
1
alias_method :create?, :update?
-
end
-
4
module API
-
4
module Public
-
4
module V1
-
4
class SerializableCourse < JSONAPI::Serializable::Resource
-
4
class << self
-
4
def enrichment_attribute(name, enrichment_name = name)
-
44
attribute name do
-
1111
@object.latest_published_enrichment&.public_send(enrichment_name)
-
end
-
end
-
end
-
-
4
type "courses"
-
-
4
belongs_to :accredited_body do
-
105
data { @object.accrediting_provider }
-
end
-
-
4
belongs_to :provider
-
4
belongs_to :recruitment_cycle
-
-
4
attributes :accredited_body_code,
-
:age_maximum,
-
:age_minimum,
-
:bursary_amount,
-
:bursary_requirements,
-
:created_at,
-
:funding_type,
-
:gcse_subjects_required,
-
:level,
-
:name,
-
:program_type,
-
:qualifications,
-
:scholarship_amount,
-
:study_mode,
-
:uuid,
-
:degree_grade,
-
:degree_subject_requirements,
-
:accept_pending_gcse,
-
:accept_gcse_equivalency,
-
:accept_english_gcse_equivalency,
-
:accept_maths_gcse_equivalency,
-
:accept_science_gcse_equivalency,
-
:additional_gcse_equivalencies
-
-
4
attribute :about_accredited_body do
-
101
@object.accrediting_provider_description
-
end
-
-
4
attribute :applications_open_from do
-
101
@object.applications_open_from&.iso8601
-
end
-
-
4
attribute :changed_at do
-
101
@object.changed_at&.iso8601
-
end
-
-
4
attribute :code do
-
101
@object.course_code
-
end
-
-
4
attribute :created_at do
-
101
@object.created_at&.iso8601
-
end
-
-
4
attribute :findable do
-
101
@object.findable?
-
end
-
-
4
attribute :has_early_career_payments do
-
101
@object.has_early_career_payments?
-
end
-
-
4
attribute :has_scholarship do
-
101
@object.has_scholarship?
-
end
-
-
4
attribute :has_vacancies do
-
101
@object.has_vacancies?
-
end
-
-
4
attribute :is_send do
-
101
@object.is_send?
-
end
-
-
4
attribute :last_published_at do
-
101
@object.last_published_at&.iso8601
-
end
-
-
4
attribute :open_for_applications do
-
101
@object.open_for_applications?
-
end
-
-
4
attribute :required_qualifications_english do
-
101
@object.english
-
end
-
-
4
attribute :required_qualifications_maths do
-
101
@object.maths
-
end
-
-
4
attribute :required_qualifications_science do
-
101
@object.science
-
end
-
-
4
attribute :running do
-
101
@object.findable?
-
end
-
-
4
attribute :start_date do
-
101
@object.start_date&.strftime("%B %Y")
-
end
-
-
4
attribute :state do
-
101
@object.content_status
-
end
-
-
4
attribute :summary do
-
101
@object.description
-
end
-
-
4
attribute :subject_codes do
-
101
@object.subjects.pluck(:subject_code).compact
-
end
-
-
4
attribute :required_qualifications do
-
101
@object.required_qualifications
-
end
-
-
4
enrichment_attribute :about_course
-
4
enrichment_attribute :course_length
-
4
enrichment_attribute :fee_details
-
4
enrichment_attribute :fee_international
-
4
enrichment_attribute :fee_domestic, :fee_uk_eu
-
4
enrichment_attribute :financial_support
-
4
enrichment_attribute :how_school_placements_work
-
4
enrichment_attribute :interview_process
-
4
enrichment_attribute :other_requirements
-
4
enrichment_attribute :personal_qualities
-
4
enrichment_attribute :salary_details
-
end
-
end
-
end
-
end
-
4
module API
-
4
module Public
-
4
module V1
-
4
class SerializableLocation < JSONAPI::Serializable::Resource
-
4
extend JSONAPI::Serializable::Resource::ConditionalFields
-
-
4
type "locations"
-
-
33
belongs_to :course, unless: -> { @course.nil? } do
-
32
data { @course }
-
end
-
-
33
belongs_to :location_status, unless: -> { @location_statuses.nil? } do
-
30
data do
-
# NOTE: This is using arrays otherwise it results in incurring N + 1
-
2
@location_statuses.find do |location_status|
-
3
location_status.site_id == @object.id
-
end
-
end
-
end
-
-
4
belongs_to :provider
-
4
belongs_to :recruitment_cycle
-
-
4
attributes :code,
-
:urn,
-
:latitude,
-
:longitude,
-
:postcode,
-
:region_code,
-
:uuid
-
-
4
attribute :name do
-
29
@object.location_name
-
end
-
-
4
attribute :city do
-
29
@object.address3
-
end
-
-
4
attribute :county do
-
29
@object.address4
-
end
-
-
4
attribute :street_address_1 do
-
29
@object.address1
-
end
-
-
4
attribute :street_address_2 do
-
29
@object.address2
-
end
-
end
-
end
-
end
-
end
-
4
module API
-
4
module Public
-
4
module V1
-
4
class SerializableLocationStatus < JSONAPI::Serializable::Resource
-
4
extend JSONAPI::Serializable::Resource::ConditionalFields
-
-
4
type "location_statuses"
-
-
4
attributes :publish,
-
:status
-
-
4
attribute :vacancy_status do
-
2
@object.vac_status
-
end
-
-
4
attribute :has_vacancies do
-
2
@object.has_vacancies?
-
end
-
end
-
end
-
end
-
end
-
4
module API
-
4
module Public
-
4
module V1
-
4
class SerializableProvider < JSONAPI::Serializable::Resource
-
4
type "providers"
-
-
4
belongs_to :recruitment_cycle
-
-
4
attributes :ukprn,
-
:urn,
-
:postcode,
-
:provider_type,
-
:region_code,
-
:train_with_disability,
-
:train_with_us,
-
:website,
-
:latitude,
-
:longitude,
-
:telephone,
-
:email,
-
:can_sponsor_skilled_worker_visa,
-
:can_sponsor_student_visa
-
-
4
attribute :accredited_body do
-
63
@object.accredited_body?
-
end
-
-
4
attribute :changed_at do
-
63
@object.changed_at.iso8601
-
end
-
-
4
attribute :city do
-
63
@object.address3
-
end
-
-
4
attribute :code do
-
63
@object.provider_code
-
end
-
-
4
attribute :county do
-
63
@object.address4
-
end
-
-
4
attribute :created_at do
-
63
@object.created_at.iso8601
-
end
-
-
4
attribute :name do
-
64
@object.provider_name
-
end
-
-
4
attribute :street_address_1 do
-
63
@object.address1
-
end
-
-
4
attribute :street_address_2 do
-
63
@object.address2
-
end
-
end
-
end
-
end
-
end
-
2
module API
-
2
module Public
-
2
module V1
-
2
class SerializableProviderSuggestion < JSONAPI::Serializable::Resource
-
2
type "provider_suggestions"
-
-
2
attributes :ukprn,
-
:urn
-
-
2
attribute :code do
-
1
@object.provider_code
-
end
-
-
2
attribute :name do
-
1
@object.provider_name
-
end
-
end
-
end
-
end
-
end
-
4
module API
-
4
module Public
-
4
module V1
-
4
class SerializableRecruitmentCycle < JSONAPI::Serializable::Resource
-
4
type "recruitment_cycles"
-
-
4
attributes :application_start_date,
-
:application_end_date
-
-
4
attribute :year do
-
10
@object.year.to_i
-
end
-
end
-
end
-
end
-
end
-
4
module API
-
4
module Public
-
4
module V1
-
4
class SerializableSubject < JSONAPI::Serializable::Resource
-
4
type "subjects"
-
-
4
attribute :name do
-
191
@object.subject_name
-
end
-
-
4
attribute :code do
-
145
@object.subject_code
-
end
-
-
4
attribute :bursary_amount do
-
145
@object.financial_incentive&.bursary_amount
-
end
-
-
4
attribute :early_career_payments do
-
145
@object.financial_incentive&.early_career_payments
-
end
-
-
4
attribute :scholarship do
-
145
@object.financial_incentive&.scholarship
-
end
-
-
4
attribute :subject_knowledge_enhancement_course_available do
-
145
@object.financial_incentive&.subject_knowledge_enhancement_course_available
-
end
-
end
-
end
-
end
-
end
-
4
module API
-
4
module Public
-
4
module V1
-
4
class SerializableSubjectArea < JSONAPI::Serializable::Resource
-
4
type "subject_areas"
-
-
4
has_many :subjects
-
-
4
attributes :name, :typename
-
end
-
end
-
end
-
end
-
4
module API
-
4
module Public
-
4
module V1
-
4
class SerializerService
-
4
include ServicePattern
-
-
4
def call
-
{
-
63
Course: API::Public::V1::SerializableCourse,
-
Provider: API::Public::V1::SerializableProvider,
-
RecruitmentCycle: API::Public::V1::SerializableRecruitmentCycle,
-
Site: API::Public::V1::SerializableLocation,
-
SiteStatus: API::Public::V1::SerializableLocationStatus,
-
Subject: API::Public::V1::SerializableSubject,
-
SubjectArea: API::Public::V1::SerializableSubjectArea,
-
PrimarySubject: API::Public::V1::SerializableSubject,
-
SecondarySubject: API::Public::V1::SerializableSubject,
-
ModernLanguagesSubject: API::Public::V1::SerializableSubject,
-
FurtherEducationSubject: API::Public::V1::SerializableSubject,
-
}
-
end
-
end
-
end
-
end
-
end
-
4
module API
-
4
module V3
-
4
class SerializableCourse < JSONAPI::Serializable::Resource
-
4
include TimeFormat
-
4
include JsonapiCourseCacheKeyHelper
-
-
4
class << self
-
4
def enrichment_attribute(name, enrichment_name = name)
-
48
attribute name do
-
1176
@object.enrichments
-
.select(&:published?)
-
936
.max_by { |e| [e.created_at, e.id] }
-
&.__send__(enrichment_name)
-
end
-
end
-
end
-
-
4
type "courses"
-
-
4
attributes :findable?, :open_for_applications?, :has_vacancies?,
-
:course_code, :name, :study_mode, :qualification, :description,
-
:content_status, :ucas_status, :funding_type,
-
:level, :is_send?, :english, :maths, :science, :gcse_subjects_required,
-
:age_range_in_years, :accrediting_provider,
-
:accredited_body_code, :level, :changed_at, :uuid, :program_type,
-
:accept_pending_gcse, :accept_gcse_equivalency,
-
:accept_english_gcse_equivalency, :accept_maths_gcse_equivalency,
-
:accept_science_gcse_equivalency, :additional_gcse_equivalencies,
-
:degree_grade, :additional_degree_subject_requirements,
-
:degree_subject_requirements
-
-
4
attribute :start_date do
-
98
written_month_year(@object.start_date) if @object.start_date
-
end
-
-
4
attribute :applications_open_from do
-
98
@object.applications_open_from&.iso8601
-
end
-
-
4
attribute :last_published_at do
-
98
@object.last_published_at&.iso8601
-
end
-
-
4
attribute :about_accrediting_body do
-
98
@object.accrediting_provider_description
-
end
-
-
4
attribute :provider_code do
-
103
@object.provider.provider_code
-
end
-
-
4
attribute :recruitment_cycle_year do
-
98
@object.recruitment_cycle.year
-
end
-
-
4
attribute :provider_type do
-
98
@object.provider.provider_type
-
end
-
-
4
belongs_to :provider
-
4
belongs_to :accrediting_provider
-
-
4
has_many :site_statuses
-
4
has_many :sites
-
4
has_many :subjects
-
-
4
enrichment_attribute :about_course
-
4
enrichment_attribute :course_length
-
4
enrichment_attribute :fee_details
-
4
enrichment_attribute :fee_international
-
4
enrichment_attribute :fee_uk_eu
-
4
enrichment_attribute :financial_support
-
4
enrichment_attribute :how_school_placements_work
-
4
enrichment_attribute :interview_process
-
4
enrichment_attribute :other_requirements
-
4
enrichment_attribute :personal_qualities
-
4
enrichment_attribute :required_qualifications
-
4
enrichment_attribute :salary_details
-
end
-
end
-
end
-
4
module API
-
4
module V3
-
4
class SerializableProvider < JSONAPI::Serializable::Resource
-
4
include JsonapiCacheKeyHelper
-
-
4
type "providers"
-
-
4
attributes :provider_code, :provider_name, :provider_type,
-
:longitude, :address1, :address2, :address3, :address4,
-
:postcode, :latitude, :longitude, :can_sponsor_student_visa,
-
:can_sponsor_skilled_worker_visa, :website, :train_with_us,
-
:train_with_disability, :email, :telephone
-
-
4
attribute :recruitment_cycle_year do
-
73
@object.recruitment_cycle.year
-
end
-
end
-
end
-
end
-
4
module API
-
4
module V3
-
4
class SerializableRecruitmentCycle < JSONAPI::Serializable::Resource
-
4
type "recruitment_cycles"
-
-
4
attributes :year, :application_start_date, :application_end_date
-
-
4
has_many :providers
-
end
-
end
-
end
-
4
module API
-
4
module V3
-
4
class SerializableSite < JSONAPI::Serializable::Resource
-
4
include JsonapiCacheKeyHelper
-
-
4
type "sites"
-
-
4
attributes :code, :location_name, :address1, :address2,
-
:address3, :address4, :postcode, :region_code,
-
:latitude, :longitude, :urn
-
-
4
attribute :recruitment_cycle_year do
-
27
@object.recruitment_cycle.year
-
end
-
end
-
end
-
end
-
4
module API
-
4
module V3
-
4
class SerializableSiteStatus < JSONAPI::Serializable::Resource
-
4
include JsonapiCacheKeyHelper
-
-
4
type "site_statuses"
-
4
attributes :vac_status, :publish, :status, :has_vacancies?
-
-
4
has_one :site
-
end
-
end
-
end
-
4
module API
-
4
module V3
-
4
class SerializableSubject < JSONAPI::Serializable::Resource
-
4
include JsonapiCacheKeyHelper
-
-
4
type "subjects"
-
-
4
attributes :subject_name, :subject_code
-
-
4
attribute :bursary_amount do
-
103
@object.financial_incentive&.bursary_amount
-
end
-
-
4
attribute :early_career_payments do
-
103
@object.financial_incentive&.early_career_payments
-
end
-
-
4
attribute :scholarship do
-
103
@object.financial_incentive&.scholarship
-
end
-
-
4
attribute :subject_knowledge_enhancement_course_available do
-
103
@object.financial_incentive&.subject_knowledge_enhancement_course_available
-
end
-
end
-
end
-
end
-
4
module API
-
4
module V3
-
4
class SerializableSubjectArea < JSONAPI::Serializable::Resource
-
4
type "subject_areas"
-
4
has_many :subjects
-
-
4
attributes :name
-
4
attributes :typename
-
end
-
end
-
end
-
4
module JsonapiCacheKeyHelper
-
4
def jsonapi_cache_key(options)
-
41
"#{self.class}/#{@object.cache_key_with_version} " + super(options)
-
end
-
end
-
4
module JsonapiCourseCacheKeyHelper
-
# When a course has sites updated the course's 'changed_at' field is touched
-
# We need to add the 'changed_at' value to the cache key so that the cache is refreshed
-
4
def jsonapi_cache_key(options)
-
59
"#{self.class}/#{@object.cache_key_with_version}/#{@object.changed_at} " + super(options)
-
end
-
end
-
1
class AccessRequestApprovalService
-
1
def self.call(access_request)
-
14
new(access_request).call
-
end
-
-
1
def initialize(access_request)
-
14
@access_request = access_request
-
end
-
-
1
def call
-
14
target_user = User.find_or_create_by!(email: @access_request.email_address.downcase) do |user|
-
8
user.first_name = @access_request.first_name
-
8
user.last_name = @access_request.last_name
-
8
user.invite_date_utc = Time.now.utc
-
end
-
-
14
providers_missing_on_target_user = @access_request.requester.providers - target_user.providers
-
14
target_user.providers << providers_missing_on_target_user
-
-
14
@access_request.approve
-
end
-
end
-
1
class AllocationCopyDataService
-
1
include ServicePattern
-
-
1
def initialize(allocation_cycle_year:, summary: false)
-
1
@allocation_cycle_year = allocation_cycle_year
-
1
@summary = summary
-
end
-
-
1
def call
-
1
copied = 0
-
1
skipped = 0
-
-
1
Allocation.where(recruitment_cycle_id: previous_allocation_cycle.id).find_each do |prev_alloc|
-
2
if prev_alloc.confirmed_number_of_places.to_i.zero? ||
-
allocation_exists?(prev_alloc)
-
skipped += 1
-
else
-
2
provider = find_provider(prev_alloc)
-
2
accredited_body = find_accredited_body(prev_alloc)
-
2
create_allocation(prev_alloc, provider, accredited_body)
-
2
copied += 1
-
end
-
end
-
-
1
if @summary
-
Rails.logger.info "###################################"
-
Rails.logger.info "## Allocation copying task complete"
-
Rails.logger.info { "# copied: #{copied}" }
-
Rails.logger.info { "# skipped: #{skipped}" }
-
Rails.logger.info "###################################"
-
end
-
end
-
-
1
private
-
-
1
def create_allocation(prev_alloc, provider, accredited_body)
-
2
Allocation.create!(
-
provider_code: prev_alloc.provider_code,
-
accredited_body_code: prev_alloc.accredited_body_code,
-
recruitment_cycle_id: allocation_cycle.id,
-
-
provider_id: provider.id,
-
accredited_body_id: accredited_body.id,
-
-
number_of_places: prev_alloc.confirmed_number_of_places,
-
confirmed_number_of_places: nil,
-
)
-
end
-
-
1
def find_provider(prev_alloc)
-
# Fetch new provider with same code for current recruitment cycle
-
2
Provider.where(
-
provider_code: prev_alloc.provider_code,
-
recruitment_cycle_id: allocation_cycle.id,
-
).first!
-
end
-
-
1
def find_accredited_body(prev_alloc)
-
# Fetch new accredited_body with same code for current recruitment cycle
-
2
Provider.where(
-
provider_code: prev_alloc.accredited_body_code,
-
recruitment_cycle_id: allocation_cycle.id,
-
accrediting_provider: :accredited_body,
-
).first!
-
end
-
-
1
def allocation_exists?(prev_alloc)
-
2
Allocation.exists?(provider_code: prev_alloc.provider_code,
-
accredited_body_code: prev_alloc.accredited_body_code,
-
recruitment_cycle_id: allocation_cycle.id)
-
end
-
-
1
def allocation_cycle
-
9
@allocation_cycle ||= RecruitmentCycle.where(year: @allocation_cycle_year).first!
-
end
-
-
1
def previous_allocation_cycle
-
1
allocation_cycle.previous
-
end
-
end
-
1
require "csv"
-
-
1
class AllocationImporterService
-
1
attr_reader :path_to_csv
-
-
1
def initialize(path_to_csv:)
-
4
@path_to_csv = path_to_csv
-
end
-
-
1
def execute
-
5
rows.each do |row|
-
5
training_provider = Provider.where(recruitment_cycle_id: 1).find_by(provider_code: row["provider_code"])
-
5
accredited_body_provider = Provider.where(recruitment_cycle_id: 1).find_by(provider_code: row["accredited_body_code"])
-
-
5
if training_provider.blank?
-
2
raise "Training Provider with code: #{row['provider_code']} not found"
-
end
-
-
3
if accredited_body_provider.blank?
-
raise "Accredited Body Provider with code: #{row['accredited_body_code']} not found"
-
end
-
-
3
Rails.logger.info { "Importing training_provider: #{training_provider.provider_code} and accredited_body: #{accredited_body_provider.provider_code}" }
-
-
3
allocation = Allocation.find_or_initialize_by(
-
provider: training_provider,
-
accredited_body: accredited_body_provider,
-
recruitment_cycle_id: 1,
-
provider_code: training_provider.provider_code,
-
accredited_body_code: accredited_body_provider.provider_code,
-
)
-
-
3
allocation.number_of_places = row["number_of_places"]
-
-
3
if allocation.changed?
-
2
allocation.save!
-
end
-
end
-
end
-
-
1
private
-
-
1
def rows
-
5
@rows ||= CSV.read(path_to_csv, headers: :first_row)
-
end
-
end
-
2
class AllocationReportingService
-
2
def initialize(recruitment_cycle_scope: RecruitmentCycle)
-
11
@current = recruitment_cycle_scope
-
11
@previous = recruitment_cycle_scope.previous
-
end
-
-
2
class << self
-
2
def call(recruitment_cycle_scope:)
-
11
new(recruitment_cycle_scope: recruitment_cycle_scope).call
-
end
-
end
-
-
2
def call
-
{
-
11
previous: reporting(recruitment_cycle: @previous),
-
current: reporting(recruitment_cycle: @current),
-
}
-
end
-
-
2
private_class_method :new
-
-
2
private
-
-
2
def reporting(recruitment_cycle: RecruitmentCycle)
-
22
requested_allocations = recruitment_cycle.allocations.not_declined
-
22
distinct_requested_allocations = requested_allocations.distinct
-
{
-
22
total: {
-
allocations: requested_allocations.count,
-
distinct_accredited_bodies: distinct_requested_allocations.select(:accredited_body_id).count,
-
distinct_providers: distinct_requested_allocations.select(:provider_id).count,
-
number_of_places: requested_allocations.sum(:number_of_places),
-
},
-
}
-
end
-
end
-
3
module Allocations
-
3
class Create
-
3
attr_accessor :object
-
-
3
def initialize(params)
-
4
@object = Allocation.new(params)
-
-
4
set_codes
-
4
set_recruitment_cycle
-
4
set_number_of_places
-
-
4
object
-
end
-
-
3
def execute
-
4
object.save
-
end
-
-
3
private
-
-
3
def set_number_of_places
-
4
other_request_type_number = 0
-
-
4
if object.number_of_places.nil?
-
3
object.number_of_places = if object.repeat?
-
1
object.previous&.number_of_places || 0
-
else
-
2
other_request_type_number
-
end
-
end
-
end
-
-
3
def set_codes
-
4
object.accredited_body_code ||= accredited_body_code
-
4
object.provider_code ||= provider_code
-
end
-
-
3
def accredited_body_code
-
4
object.accredited_body.provider_code
-
end
-
-
3
def provider_code
-
4
return unless object.provider
-
-
4
object.provider.provider_code
-
end
-
-
3
def set_recruitment_cycle
-
4
object.recruitment_cycle_id ||= RecruitmentCycle.find_by(year: Allocation::ALLOCATION_CYCLE_YEAR).id
-
end
-
end
-
end
-
1
module Allocations
-
1
class Update
-
1
attr_accessor :object, :params
-
-
1
def initialize(allocation, params)
-
13
@object = allocation
-
13
@params = params
-
-
13
set_number_of_places
-
13
set_request_type
-
-
13
object
-
end
-
-
1
def execute
-
13
object.save
-
end
-
-
1
private
-
-
1
def set_number_of_places
-
13
object.number_of_places = case params[:request_type]
-
when "declined"
-
3
0
-
when "repeat"
-
6
object.previous&.number_of_places || 0
-
else
-
4
params[:number_of_places]
-
end
-
end
-
-
1
def set_request_type
-
13
object.request_type = params[:request_type]
-
end
-
end
-
end
-
4
class AuthenticationService
-
4
attr_accessor :encoded_token, :user
-
-
4
def initialize(logger:)
-
23
@logger = logger
-
end
-
-
4
class << self
-
4
DFE_SIGNIN = "dfe_signin".freeze
-
4
PERSONA = "persona".freeze
-
4
MAGIC_LINK = "magic_link".freeze
-
-
4
def mode
-
1162
case Settings.authentication.mode
-
when MAGIC_LINK
-
MAGIC_LINK
-
when PERSONA
-
PERSONA
-
else
-
1162
DFE_SIGNIN
-
end
-
end
-
-
4
def dfe_signin?
-
223
mode == DFE_SIGNIN
-
end
-
-
4
def magic_link?
-
596
mode == MAGIC_LINK
-
end
-
-
4
def persona?
-
366
mode == PERSONA
-
end
-
end
-
-
4
def execute(encoded_token)
-
23
@encoded_token = encoded_token
-
23
@user = find_user_by_sign_in_user_id || find_user_by_email
-
23
update_user_information if user
-
23
user
-
end
-
-
4
private
-
-
4
attr_reader :logger
-
-
4
def decoded_token
-
217
@decoded_token ||= Token::DecodeService.call(
-
encoded_token: encoded_token,
-
secret: Settings.authentication.secret,
-
algorithm: Settings.authentication.algorithm,
-
audience: Settings.authentication.audience,
-
issuer: Settings.authentication.issuer,
-
subject: Settings.authentication.subject,
-
)
-
end
-
-
4
def email_from_token
-
61
decoded_token["email"]&.downcase
-
end
-
-
4
def sign_in_user_id_from_token
-
82
decoded_token["sign_in_user_id"]
-
end
-
-
4
def first_name_from_token
-
37
decoded_token["first_name"]
-
end
-
-
4
def last_name_from_token
-
37
decoded_token["last_name"]
-
end
-
-
4
def update_user_information
-
19
update_user_first_name
-
19
update_user_last_name
-
19
update_user_sign_in_id
-
19
update_user_email
-
19
user.save!
-
end
-
-
4
def find_user_by_email
-
16
if email_from_token.blank?
-
2
log_message(:debug, user, "No email in token")
-
2
return
-
end
-
-
14
if (user = User.find_by("lower(email) = ?", email_from_token))
-
9
log_message(:info, user, "User found by email address")
-
end
-
-
14
user
-
end
-
-
4
def find_user_by_sign_in_user_id
-
23
if sign_in_user_id_from_token.blank?
-
2
log_message(:debug, user, "No sign_in_user_id in token")
-
2
return
-
end
-
-
21
if (user = User.find_by(sign_in_user_id: sign_in_user_id_from_token))
-
13
log_message(:info, user, "User found from sign_in_user_id in token", { sign_in_user_id: sign_in_user_id_from_token })
-
end
-
-
21
user
-
end
-
-
4
def user_email_does_not_match_token?
-
19
user.email&.downcase != email_from_token
-
end
-
-
4
def user_sign_in_id_does_not_match_token?
-
19
return unless user
-
-
19
user.sign_in_user_id != sign_in_user_id_from_token
-
end
-
-
4
def update_user_email
-
19
return unless user_email_does_not_match_token?
-
-
6
if (duplicate_user = find_user_by_email)
-
# Change: bob@gmail.com => bob_1634828853_gmail.com@example.com
-
3
new_email = "#{duplicate_user.email.gsub(/\@/, "_#{Time.now.to_i}_")}@example.com"
-
-
3
duplicate_user.update!(email: new_email)
-
end
-
-
6
log_message(:debug, user, "Updating user email for", { new_email_md5: "MD5:#{obfuscate_email(email_from_token)}" })
-
6
user.email = email_from_token
-
end
-
-
4
def update_user_sign_in_id
-
19
return unless user_sign_in_id_does_not_match_token?
-
-
6
user.sign_in_user_id = sign_in_user_id_from_token
-
end
-
-
4
def update_user_first_name
-
19
if first_name_from_token.blank?
-
1
log_message(:debug, user, "No first name in token")
-
1
return
-
end
-
-
18
user.first_name = first_name_from_token
-
end
-
-
4
def update_user_last_name
-
19
if last_name_from_token.blank?
-
1
log_message(:debug, user, "No last name in token")
-
1
return
-
end
-
-
18
user.last_name = last_name_from_token
-
end
-
-
4
def log_safe_user(user)
-
30
user.slice(
-
"id",
-
"state",
-
"first_login_date_utc",
-
"last_login_date_utc",
-
"sign_in_user_id",
-
"welcome_email_date_utc",
-
"invite_date_utc",
-
"accept_terms_date_utc",
-
).merge("email_md5" => obfuscate_email(user.email))
-
end
-
-
4
def obfuscate_email(email)
-
36
Digest::MD5.hexdigest(email)
-
end
-
-
4
def log_message(level, user, message, extra_attributes = {})
-
34
attributes = extra_attributes
-
34
attributes = attributes.merge(user: log_safe_user(user)) if user
-
-
34
logger.public_send(level) do
-
5
"#{message} #{attributes} "
-
end
-
end
-
end
-
2
class CourseAttributeFormatterService
-
2
def initialize(name:, value:)
-
24
@name = name
-
24
@value = value
-
end
-
-
2
class << self
-
2
def call(**args)
-
24
new(args).call
-
end
-
end
-
-
2
def call
-
24
return age_range_value if age_range?
-
22
return qualification_value if qualification?
-
17
return study_mode_value if study_mode?
-
13
return entry_requirements_value if entry_requirements?
-
-
1
value
-
end
-
-
2
private_class_method :new
-
-
2
private
-
-
2
attr_reader :name, :value
-
-
2
def age_range?
-
24
name == "age_range_in_years"
-
end
-
-
2
def age_range_value
-
2
strip_underscores
-
end
-
-
2
def qualification?
-
22
name == "qualification"
-
end
-
-
2
def qualification_value
-
5
I18n.t("course.values.qualification.#{value}")
-
end
-
-
2
def study_mode?
-
17
name == "study_mode"
-
end
-
-
2
def study_mode_value
-
4
strip_underscores
-
end
-
-
2
def entry_requirements?
-
13
%w[maths english science].include?(name)
-
end
-
-
2
def entry_requirements_value
-
12
I18n.t("course.values.entry_requirements.#{value}")
-
end
-
-
2
def strip_underscores
-
6
if value
-
4
value.tr("_", " ")
-
else
-
2
"unknown"
-
end
-
end
-
end
-
3
class CourseReportingService
-
3
def initialize(courses_scope: Course)
-
11
@courses = courses_scope.distinct
-
11
@findable_courses = @courses.findable
-
11
@open_courses = @findable_courses.with_vacancies
-
11
@closed_courses = @findable_courses.where.not(id: @open_courses)
-
end
-
-
3
class << self
-
3
def call(courses_scope:)
-
11
new(courses_scope: courses_scope).call
-
end
-
end
-
-
3
def call
-
{
-
11
total: {
-
all: @courses.count,
-
non_findable: @courses.count - @findable_courses.count,
-
all_findable: @findable_courses.count,
-
},
-
findable_total: {
-
open: @open_courses.count,
-
closed: @closed_courses.count,
-
},
-
provider_type: { **group_by_count(:provider_type) },
-
program_type: { **group_by_count(:program_type) },
-
-
study_mode: { **group_by_count(:study_mode) },
-
qualification: { **group_by_count(:qualification) },
-
is_send: { **group_by_count(:is_send) },
-
-
subject: { **group_by_subject_count },
-
}
-
end
-
-
3
private_class_method :new
-
-
3
private
-
-
3
def group_by_subject_count
-
11
open = CourseSubject.where(course_id: @open_courses).group(:subject_id).count
-
11
closed = CourseSubject.where(course_id: @closed_courses).group(:subject_id).count
-
-
{
-
11
open: Subject.active.map { |sub|
-
506
x = {}
-
506
x[sub.subject_name] = open[sub.id] || 0
-
506
x
-
} .reduce({}, :merge),
-
closed: Subject.active.map { |sub|
-
506
x = {}
-
506
x[sub.subject_name] = closed[sub.id] || 0
-
506
x
-
} .reduce({}, :merge),
-
}
-
end
-
-
3
def group_by_count(column)
-
55
open = @open_courses.group(column).count
-
55
closed = @closed_courses.group(column).count
-
-
55
case column
-
when :provider_type
-
{
-
11
open: Provider.provider_types.map { |key, value|
-
33
x = {}
-
33
x[key.to_sym] = open[value] || 0
-
33
x
-
} .reduce({}, :merge),
-
closed: Provider.provider_types.map { |key, value|
-
33
x = {}
-
33
x[key.to_sym] = closed[value] || 0
-
33
x
-
} .reduce({}, :merge),
-
}
-
when :program_type, :study_mode, :qualification
-
{
-
33
open: Course.send(column.to_s.pluralize).map { |key, _value|
-
143
x = {}
-
143
x[key.to_sym] = open[key] || 0
-
143
x
-
} .reduce({}, :merge),
-
closed: Course.send(column.to_s.pluralize).map { |key, _value|
-
143
x = {}
-
143
x[key.to_sym] = closed[key] || 0
-
143
x
-
} .reduce({}, :merge),
-
}
-
when :is_send
-
{
-
11
open: { yes: open[true] || 0, no: open[false] || 0 },
-
closed: { yes: closed[true] || 0, no: closed[false] || 0 },
-
}
-
end
-
end
-
end
-
3
class CourseSearchService
-
3
def initialize(
-
filter:,
-
sort: nil,
-
course_scope: Course
-
)
-
89
@filter = filter || {}
-
89
@course_scope = course_scope
-
89
@sort = Set.new(sort&.split(","))
-
end
-
-
3
class << self
-
3
def call(**args)
-
89
new(args).call
-
end
-
end
-
-
3
def call
-
89
scope = course_scope
-
89
scope = scope.with_salary if funding_filter_salary?
-
89
scope = scope.with_qualifications(qualifications) if qualifications.any?
-
89
scope = scope.with_vacancies if has_vacancies?
-
89
scope = scope.findable if findable?
-
89
scope = scope.with_study_modes(study_types) if study_types.any?
-
89
scope = scope.with_subjects(subject_codes) if subject_codes.any?
-
89
scope = scope.with_provider_name(provider_name) if provider_name.present?
-
89
scope = scope.with_send if send_courses_filter?
-
89
scope = scope.within(filter[:radius], origin: origin) if locations_filter?
-
89
scope = scope.with_funding_types(funding_types) if funding_types.any?
-
89
scope = scope.with_degree_grades(degree_grades) if degree_grades.any?
-
89
scope = scope.changed_since(filter[:updated_since]) if updated_since_filter?
-
89
scope = scope.provider_can_sponsor_visa if can_sponsor_visa_filter?
-
-
# The 'where' scope will remove duplicates
-
# An outer query is required in the event the provider name is present.
-
# This prevents 'PG::InvalidColumnReference: ERROR: for SELECT DISTINCT, ORDER BY expressions must appear in select list'
-
89
outer_scope = Course.includes(
-
:enrichments,
-
:financial_incentives,
-
course_subjects: [:subject],
-
site_statuses: [:site],
-
provider: %i[recruitment_cycle ucas_preferences],
-
).where(id: scope.select(:id))
-
-
89
if provider_name.present?
-
1
outer_scope = outer_scope
-
.accredited_body_order(provider_name)
-
.ascending_canonical_order
-
88
elsif sort_by_provider_ascending?
-
1
outer_scope = outer_scope.ascending_canonical_order
-
1
outer_scope = outer_scope.select("provider.provider_name", "course.*")
-
87
elsif sort_by_provider_descending?
-
1
outer_scope = outer_scope.descending_canonical_order
-
1
outer_scope = outer_scope.select("provider.provider_name", "course.*")
-
86
elsif sort_by_distance?
-
6
outer_scope = outer_scope.joins(courses_with_distance_from_origin)
-
6
outer_scope = outer_scope.joins(:provider)
-
6
outer_scope = outer_scope.select("course.*, distance, #{Course.sanitize_sql(distance_with_university_area_adjustment)}")
-
-
outer_scope =
-
6
if expand_university?
-
2
outer_scope.order(:boosted_distance)
-
else
-
4
outer_scope.order(:distance)
-
end
-
end
-
-
89
outer_scope
-
end
-
-
3
private_class_method :new
-
-
3
private
-
-
3
def expand_university?
-
6
filter[:expand_university].to_s.downcase == "true"
-
end
-
-
3
def distance_with_university_area_adjustment
-
6
university_provider_type = Provider.provider_types[:university]
-
6
university_location_area_radius = 10
-
6
<<~EOSQL.gsub(/\s+/m, " ").strip
-
(CASE
-
WHEN provider.provider_type = '#{university_provider_type}'
-
THEN (distance - #{university_location_area_radius})
-
ELSE distance
-
END) as boosted_distance
-
EOSQL
-
end
-
-
3
def locatable_sites
-
12
site_statuses = SiteStatus.arel_table
-
12
sites = Site.arel_table
-
-
# Create virtual table with sites and site statuses
-
12
site_statuses.join(sites).on(site_statuses[:site_id].eq(sites[:id]))
-
.where(site_statuses_criteria(site_statuses))
-
.where(has_been_geocoded_criteria(sites))
-
.where(locatable_address_criteria(sites))
-
end
-
-
3
def site_statuses_criteria(site_statuses)
-
# Only running and published site statuses
-
12
running_and_published_criteria = site_statuses[:status].eq(SiteStatus.statuses[:running]).and(site_statuses[:publish].eq(SiteStatus.publishes[:published]))
-
-
12
if has_vacancies?
-
# Only site statuses with vacancies
-
running_and_published_criteria
-
.and(site_statuses[:vac_status])
-
.eq_any([
-
SiteStatus.vac_statuses[:full_time_vacancies],
-
SiteStatus.vac_statuses[:part_time_vacancies],
-
SiteStatus.vac_statuses[:both_full_time_and_part_time_vacancies],
-
])
-
else
-
12
running_and_published_criteria
-
end
-
end
-
-
3
def has_been_geocoded_criteria(sites)
-
# we only want sites that have been geocoded
-
12
sites[:latitude].not_eq(nil).and(sites[:longitude].not_eq(nil))
-
end
-
-
3
def locatable_address_criteria(sites)
-
# only sites that have a locatable address
-
# there are some sites with no address1 or postcode that cannot be
-
# accurately geocoded. We don't want to return these as the closest site.
-
# This should be removed once the data is fixed
-
12
sites[:address1].not_eq("").or(sites[:postcode].not_eq(""))
-
end
-
-
3
def course_id_with_lowest_locatable_distance
-
# select course_id and nearest site with shortest distance from origin
-
# as courses may have multiple sites
-
# this will remove duplicates by aggregating on course_id
-
12
origin_lat_long = Struct.new(:lat, :lng).new(origin[0].to_f, origin[1].to_f)
-
12
lowest_locatable_distance = Arel.sql("MIN#{Site.sanitize_sql(Site.distance_sql(origin_lat_long))} as distance")
-
12
locatable_sites.project(:course_id, lowest_locatable_distance).group(:course_id)
-
end
-
-
3
def distance_table
-
# form a temporary table with results
-
12
Arel::Nodes::TableAlias.new(
-
Arel.sql(
-
format("(%s)", course_id_with_lowest_locatable_distance.to_sql),
-
), "distances"
-
)
-
end
-
-
3
def courses_with_distance_from_origin
-
# grab courses table and join with the above result set
-
# so distances from origin are now available
-
# we can then sort by distance from the given origin
-
6
courses_table = Course.arel_table
-
6
courses_table.join(distance_table).on(courses_table[:id].eq(distance_table[:course_id])).join_sources
-
end
-
-
3
def locations_filter?
-
89
filter.key?(:latitude) &&
-
filter.key?(:longitude) &&
-
filter.key?(:radius)
-
end
-
-
3
def sort_by_provider_ascending?
-
88
sort == Set["name", "provider.provider_name"]
-
end
-
-
3
def sort_by_provider_descending?
-
87
sort == Set["-name", "-provider.provider_name"]
-
end
-
-
3
def sort_by_distance?
-
86
sort == Set["distance"]
-
end
-
-
3
def origin
-
25
[filter[:latitude], filter[:longitude]]
-
end
-
-
3
attr_reader :sort, :filter, :course_scope
-
-
3
def funding_filter_salary?
-
89
filter[:funding] == "salary"
-
end
-
-
3
def qualifications
-
90
return [] if filter[:qualification].blank?
-
-
2
filter[:qualification].split(",")
-
end
-
-
3
def has_vacancies?
-
101
filter[:has_vacancies].to_s.downcase == "true"
-
end
-
-
3
def findable?
-
89
filter[:findable].to_s.downcase == "true"
-
end
-
-
3
def study_types
-
93
return [] if filter[:study_type].blank?
-
-
8
filter[:study_type].split(",")
-
end
-
-
3
def funding_types
-
109
return [] if filter[:funding_type].blank?
-
-
40
filter[:funding_type].split(",")
-
end
-
-
3
def degree_grades
-
93
return [] if filter[:degree_grade].blank?
-
-
8
filter[:degree_grade].split(",")
-
end
-
-
3
def subject_codes
-
107
return [] if filter[:subjects].blank?
-
-
36
filter[:subjects].split(",")
-
end
-
-
3
def provider_name
-
180
return [] if filter[:"provider.provider_name"].blank?
-
-
4
filter[:"provider.provider_name"]
-
end
-
-
3
def send_courses_filter?
-
89
filter[:send_courses].to_s.downcase == "true"
-
end
-
-
3
def updated_since_filter?
-
89
filter[:updated_since].present?
-
end
-
-
3
def can_sponsor_visa_filter?
-
89
filter[:can_sponsor_visa].to_s.downcase == "true"
-
end
-
end
-
4
class CourseSerializersService
-
4
def initialize(
-
course_serializer: API::V3::SerializableCourse,
-
subject_serializer: API::V3::SerializableSubject,
-
primary_subject_serializer: API::V3::SerializableSubject,
-
secondary_subject_serializer: API::V3::SerializableSubject,
-
modern_languages_subject_serializer: API::V3::SerializableSubject,
-
further_education_subject_serializer: API::V3::SerializableSubject,
-
site_status_serializer: API::V3::SerializableSiteStatus,
-
site_serializer: API::V3::SerializableSite,
-
provider_serializer: API::V3::SerializableProvider,
-
recruitment_cycle_serializer: API::V3::SerializableRecruitmentCycle,
-
subject_area_serializer: API::V3::SerializableSubjectArea
-
)
-
72
@course_serializer = course_serializer
-
72
@subject_serializer = subject_serializer
-
72
@primary_subject_serializer = primary_subject_serializer
-
72
@secondary_subject_serializer = secondary_subject_serializer
-
72
@modern_languages_subject_serializer = modern_languages_subject_serializer
-
72
@further_education_subject_serializer = further_education_subject_serializer
-
72
@site_serializer = site_serializer
-
72
@site_status_serializer = site_status_serializer
-
72
@provider_serializer = provider_serializer
-
72
@recruitment_cycle_serializer = recruitment_cycle_serializer
-
72
@subject_area_serializer = subject_area_serializer
-
end
-
-
4
def execute
-
{
-
72
Course: @course_serializer,
-
Subject: @subject_serializer,
-
PrimarySubject: @primary_subject_serializer,
-
SecondarySubject: @secondary_subject_serializer,
-
ModernLanguagesSubject: @modern_languages_subject_serializer,
-
FurtherEducationSubject: @further_education_subject_serializer,
-
SiteStatus: @site_status_serializer,
-
Site: @site_serializer,
-
Provider: @provider_serializer,
-
RecruitmentCycle: @recruitment_cycle_serializer,
-
SubjectArea: @subject_area_serializer,
-
}
-
end
-
end
-
4
module Courses
-
4
class AssignProgramTypeService
-
4
def execute(funding_type, course)
-
95
case funding_type
-
when "salary"
-
11
update_salaried_program(course)
-
when "apprenticeship"
-
9
course.program_type = :pg_teaching_apprenticeship
-
when "fee"
-
53
course.program_type = update_fee_program(course)
-
end
-
-
# NOTE: This looks like unwarranted side effects
-
95
course.save
-
end
-
-
4
private
-
-
4
def update_salaried_program(course)
-
11
course.program_type = :school_direct_salaried_training_programme
-
end
-
-
4
def update_fee_program(course)
-
53
if !course.self_accredited?
-
36
:school_direct_training_programme
-
17
elsif course.provider.scitt?
-
13
:scitt_programme
-
else
-
4
:higher_education_programme
-
end
-
end
-
end
-
end
-
4
module Courses
-
4
class AssignSubjectsService
-
4
include ServicePattern
-
-
4
attr_reader :course, :subject_ids
-
-
4
def initialize(course:, subject_ids:)
-
161
@course = course
-
161
@subject_ids = subject_ids || []
-
end
-
-
4
def call
-
161
course.errors.add(:subjects, :duplicate) if request_has_duplicate_subject_ids?
-
-
161
update_subjects
-
-
161
course.name = course.generate_name
-
161
course
-
end
-
-
4
private
-
-
4
def subjects
-
154
@subjects ||= Subject.find(@subject_ids)
-
end
-
-
4
def update_subjects
-
161
if course.further_education_course?
-
7
update_further_education_fields
-
-
7
course.subjects = [FurtherEducationSubject.instance]
-
7
course.course_subjects.first.position = 1
-
-
else
-
154
course.subjects = subjects
-
-
154
subject_ids.each_with_index do |id, index|
-
286
course.course_subjects.select { |cs| cs.subject_id == id.to_i }.first.position = index
-
end
-
end
-
end
-
-
4
def request_has_duplicate_subject_ids?
-
161
subject_ids.uniq.count != subject_ids.count
-
end
-
-
4
def update_further_education_fields
-
7
course.funding_type = "fee"
-
7
course.english = "not_required"
-
7
course.maths = "not_required"
-
7
course.science = "not_required"
-
end
-
end
-
end
-
4
module Courses
-
4
class AssignableMasterSubjectService
-
4
def initialize(
-
primary_subject: PrimarySubject,
-
secondary_subject: SecondarySubject,
-
further_education_subject: FurtherEducationSubject
-
)
-
188
@primary_subject = primary_subject
-
188
@secondary_subject = secondary_subject
-
188
@further_education_subject = further_education_subject
-
end
-
-
4
def execute(course:)
-
188
case course.level
-
when "primary"
-
112
@primary_subject.includes(:financial_incentive).all
-
when "secondary"
-
69
@secondary_subject.includes(:financial_incentive).all
-
when "further_education"
-
7
@further_education_subject.includes(:financial_incentive).all
-
end
-
end
-
end
-
end
-
1
module Courses
-
1
class AssignableSubjectService
-
1
def initialize(
-
primary_subject: PrimarySubject,
-
secondary_subject: SecondarySubject,
-
modern_language_subject: ModernLanguagesSubject,
-
further_education_subject: FurtherEducationSubject,
-
modern_languages_parent_subject: SecondarySubject.modern_languages
-
)
-
3
@primary_subject = primary_subject
-
3
@secondary_subject = secondary_subject
-
3
@modern_language_subject = modern_language_subject
-
3
@further_education_subject = further_education_subject
-
3
@modern_languages_parent_subject = modern_languages_parent_subject
-
end
-
-
1
def execute(course:)
-
3
case course.level
-
when "primary"
-
1
@primary_subject.all
-
when "secondary"
-
1
@secondary_subject.where.not(id: @modern_languages_parent_subject) + @modern_language_subject.all
-
when "further_education"
-
1
@further_education_subject.all
-
end
-
end
-
end
-
end
-
4
module Courses
-
4
class ContentStatusService
-
4
def execute(enrichment:, recruitment_cycle:)
-
1201
if enrichment.blank?
-
821
if recruitment_cycle.next?
-
3
:rolled_over
-
else
-
818
:empty
-
end
-
380
elsif enrichment.published?
-
241
:published
-
139
elsif enrichment.withdrawn?
-
21
:withdrawn
-
118
elsif enrichment.has_been_published_before?
-
49
:published_with_unpublished_changes
-
69
elsif enrichment.rolled_over?
-
2
:rolled_over
-
else
-
67
:draft
-
end
-
end
-
end
-
end
-
4
module Courses
-
4
class Copy
-
GCSE_FIELDS = [
-
4
["Accept pending GCSE", "accept_pending_gcse"],
-
["Accept GCSE equivalency", "accept_gcse_equivalency"],
-
["Accept English GCSE equivalency", "accept_english_gcse_equivalency"],
-
["Accept Maths GCSE equivalency", "accept_maths_gcse_equivalency"],
-
["Additional GCSE equivalencies", "additional_gcse_equivalencies"],
-
].freeze
-
-
SUBJECT_REQUIREMENTS_FIELDS = [
-
4
["Additional degree subject requirements", "additional_degree_subject_requirements"],
-
["Degree subject requirements", "degree_subject_requirements"],
-
].freeze
-
-
ABOUT_FIELDS = [
-
4
["About the course", "about_course"],
-
["Interview process", "interview_process"],
-
["How school placements work", "how_school_placements_work"],
-
].freeze
-
-
FEES_FIELDS = [
-
4
["Course length", "course_length"],
-
["Fee for UK students", "fee_uk_eu"],
-
["Fee for international students", "fee_international"],
-
["Fee details", "fee_details"],
-
["Financial support", "financial_support"],
-
].freeze
-
-
SALARY_FIELDS = [
-
4
["Course length", "course_length"],
-
["Salary details", "salary_details"],
-
].freeze
-
-
POST_2022_CYCLE_REQUIREMENTS_FIELDS = [
-
4
["Personal qualities", "personal_qualities"],
-
["Other requirements", "other_requirements"],
-
].freeze
-
-
PRE_2022_CYCLE_REQUIREMENTS_FIELDS = [
-
4
["Qualifications needed", "required_qualifications"],
-
["Personal qualities", "personal_qualities"],
-
["Other requirements", "other_requirements"],
-
].freeze
-
-
4
def self.get_present_fields_in_source_course(fields, source_course, course)
-
4
fields.filter_map do |name, field|
-
10
source_value = source_course.enrichments.last[field]
-
10
if source_value.present?
-
7
course.enrichments.last[field] = source_value
-
7
[name, field]
-
end
-
end
-
end
-
-
4
def self.get_boolean_fields(fields, source_course, course)
-
4
fields.select do |_name, field|
-
14
source_value = source_course[field]
-
14
course[field] = source_value if source_value.present?
-
end
-
end
-
end
-
end
-
2
module Courses
-
2
class CopyToProviderService
-
2
def initialize(sites_copy_to_course:, enrichments_copy_to_course:)
-
18
@sites_copy_to_course = sites_copy_to_course
-
18
@enrichments_copy_to_course = enrichments_copy_to_course
-
end
-
-
2
def execute(course:, new_provider:, force: false)
-
18
return unless course.rollable? || force
-
18
return if course_code_already_exists_on_provider?(course: course, new_provider: new_provider)
-
-
12
new_course = nil
-
-
12
Course.transaction do
-
12
new_course = course.dup
-
12
new_course.uuid = nil
-
12
new_course.provider = new_provider
-
12
year_differential = new_course.recruitment_cycle.year.to_i - course.recruitment_cycle.year.to_i
-
12
new_course.applications_open_from = adjusted_applications_open_from_date(course, year_differential)
-
12
new_course.start_date = course.start_date + year_differential.year
-
12
new_course.subjects = course.subjects
-
12
new_course.save!(validate: false)
-
-
12
copy_latest_enrichment_to_course(course, new_course)
-
-
12
course.sites.each do |site|
-
3
new_site = new_provider.sites.find_by(code: site.code)
-
-
3
@sites_copy_to_course.execute(new_site: new_site, new_course: new_course) if new_site.present?
-
end
-
end
-
12
new_course
-
end
-
-
2
private
-
-
2
def course_code_already_exists_on_provider?(course:, new_provider:)
-
18
new_provider.courses.with_discarded.where(course_code: course.course_code).any?
-
end
-
-
2
def copy_latest_enrichment_to_course(course, new_course)
-
12
last_enrichment = course.enrichments.most_recent.first
-
12
return if last_enrichment.blank?
-
-
12
@enrichments_copy_to_course.execute(enrichment: last_enrichment, new_course: new_course)
-
end
-
-
2
def adjusted_applications_open_from_date(course, year_differential)
-
12
next_cycle = RecruitmentCycle.next_recruitment_cycle
-
-
12
if course.applications_open_from + year_differential.year >= next_cycle.application_start_date
-
11
course.applications_open_from + year_differential.year
-
else
-
1
next_cycle.application_start_date
-
end
-
end
-
end
-
end
-
4
module Courses
-
4
class CreationService
-
4
include ServicePattern
-
-
4
attr_reader :course_params, :provider, :next_available_course_code
-
-
4
def initialize(course_params:, provider:, next_available_course_code: false)
-
142
@course_params = course_params
-
142
@provider = provider
-
142
@next_available_course_code = next_available_course_code
-
end
-
-
4
def call
-
142
build_new_course
-
end
-
-
4
private
-
-
4
def new_course
-
142
@new_course ||= provider.courses.new
-
end
-
-
4
def build_new_course
-
142
course = provider.courses.new
-
142
course.assign_attributes(course_attributes)
-
-
142
update_sites(course)
-
-
142
course.course_code = provider.next_available_course_code if next_available_course_code
-
-
142
AssignSubjectsService.call(course: course, subject_ids: subject_ids)
-
-
142
course.valid?(:new)
-
142
course.remove_carat_from_error_messages
-
-
142
course
-
end
-
-
4
def course_attributes
-
142
@course_attributes ||= course_params.to_h.symbolize_keys.slice(*permitted_new_course_attributes)
-
end
-
-
4
def permitted_new_course_attributes
-
142
@permitted_new_course_attributes ||= CoursePolicy.new(nil, new_course).permitted_new_course_attributes
-
end
-
-
4
def sites
-
18
@sites ||= provider.sites.find(site_ids)
-
end
-
-
4
def subject_ids
-
142
@subject_ids ||= course_params["subjects_ids"]
-
end
-
-
4
def site_ids
-
198
@site_ids ||= course_params["sites_ids"]
-
end
-
-
4
def update_sites(course)
-
142
return if site_ids.nil?
-
-
19
course.sites = sites if site_ids.any?
-
-
19
course.errors.add(:sites, message: "Select at least one location") if site_ids.empty?
-
end
-
end
-
end
-
4
module Courses
-
4
class Fetch
-
4
class << self
-
4
def by_code(provider_code:, course_code:)
-
8
RecruitmentCycle.current.providers
-
.find_by(provider_code: provider_code)
-
.courses
-
.find_by(course_code: course_code)
-
end
-
-
4
def by_accrediting_provider(provider)
-
# rubocop:disable Style/MultilineBlockChain
-
# rubocop:disable Style/HashTransformValues
-
64
provider
-
.courses
-
.group_by do |course|
-
97
course.accrediting_provider&.provider_name || provider.provider_name
-
rescue StandardError
-
provider.provider_name
-
end
-
57
.sort_by { |accrediting_provider, _| accrediting_provider.downcase }
-
.to_h do |provider_name, courses|
-
154
[provider_name, courses.sort_by { |course| [course.name, course.course_code] }.map(&:decorate)]
-
end
-
# rubocop:enable Style/MultilineBlockChain
-
# rubocop:enable Style/HashTransformValues
-
end
-
end
-
end
-
end
-
4
module Courses
-
4
class GenerateCourseNameService
-
4
def execute(course:)
-
192
subjects = course.subjects
-
-
192
title = if course.further_education_course?
-
9
further_education_title
-
183
elsif is_modern_lanuage_course?(subjects)
-
26
modern_language_title(subjects)
-
else
-
157
generated_title(subjects)
-
end
-
-
192
title += " (SEND)" if course.is_send?
-
192
title
-
end
-
-
4
private
-
-
4
def is_modern_lanuage_course?(subjects)
-
348
subjects.any? { |s| s == SecondarySubject.modern_languages }
-
end
-
-
4
def modern_language_title(subjects)
-
26
title = SecondarySubject.modern_languages.to_s
-
-
87
languages = subjects.select { |s| s.type == "ModernLanguagesSubject" }
-
61
languages = languages.reject { |language| language.subject_name.casecmp?("Modern languages (other)") }
-
-
26
return title if languages.empty? || languages.length >= 4
-
-
38
language_names = languages.map { |language| format_language_name(language) }
-
-
14
case language_names.length
-
when 1
-
7
title + " (#{language_names[0]})"
-
when 2
-
4
title + " (#{language_names.join(' and ')})"
-
when 3
-
3
title + " (#{language_names.join(', ')})"
-
end
-
end
-
-
4
def generated_title(subjects)
-
157
return "" if subjects.empty?
-
-
270
subjects = subjects.map { |s| format_subject_name(s) }
-
-
132
if subjects.length == 1
-
126
subjects[0]
-
else
-
6
"#{subjects[0]} with #{subjects[1]}"
-
end
-
end
-
-
4
def further_education_title
-
9
"Further education"
-
end
-
-
4
def format_subject_name(subject)
-
138
if subject.subject_name.casecmp?("Communication and media studies")
-
2
"Media studies"
-
else
-
136
subject.to_s
-
end
-
end
-
-
4
def format_language_name(language)
-
24
if language.subject_name.casecmp?("English as a second or other language")
-
3
"English"
-
else
-
21
language.to_s
-
end
-
end
-
end
-
end
-
4
module Courses
-
4
class ValidateCustomAgeRangeService
-
4
def execute(age_range_in_years, course)
-
1843
valid_age_range_regex = Regexp.new(/^(?<from>\d{1,2})_to_(?<to>\d{1,2})$/)
-
-
1843
if valid_age_range_regex.match(age_range_in_years)
-
1748
from_age = get_ages(age_range_in_years, valid_age_range_regex)["from"]
-
1748
to_age = get_ages(age_range_in_years, valid_age_range_regex)["to"]
-
1748
if from_age_invalid?(from_age) || to_age_invalid?(to_age)
-
2
course.errors.add(:age_range_in_years, "^Age range must be a school age")
-
1746
elsif to_age - from_age < 4
-
1
course.errors.add(:age_range_in_years, "^Age range must cover at least 4 years")
-
end
-
else
-
95
course.errors.add(:age_range_in_years, "^Enter an age range")
-
end
-
end
-
-
4
private
-
-
4
def get_ages(age_range_in_years, valid_age_range_regex)
-
10488
valid_age_range_regex.match(age_range_in_years)&.named_captures&.transform_values { |year| year.to_i }
-
end
-
-
4
def from_age_invalid?(from_age)
-
1748
from_age < 3 || from_age > 15
-
end
-
-
4
def to_age_invalid?(to_age)
-
1747
to_age < 7 || to_age > 19
-
end
-
end
-
end
-
1
require "csv"
-
-
1
module CSVImports
-
1
class FakeProvidersImport
-
1
attr_reader :results
-
-
1
def initialize(path_to_csv)
-
5
@results = []
-
5
@path_to_csv = path_to_csv
-
end
-
-
1
def execute
-
5
CSV.foreach(@path_to_csv, headers: true) do |row|
-
8
provider_name = row["name"]
-
8
provider_code = row["code"]
-
8
provider_type = row["type"]
-
8
is_accredited_body = ActiveModel::Type::Boolean.new.cast(row["accredited_body"])
-
-
8
service = Providers::CreateFakeProviderService.new(
-
recruitment_cycle: RecruitmentCycle.current,
-
provider_name: provider_name,
-
provider_code: provider_code,
-
provider_type: provider_type,
-
is_accredited_body: is_accredited_body,
-
)
-
-
8
@results << if service.execute
-
7
"Created provider #{provider_name}."
-
else
-
1
service.errors.join(" ")
-
end
-
end
-
end
-
end
-
end
-
2
module Enrichments
-
2
class CopyToCourseService
-
2
def execute(enrichment:, new_course:)
-
5
new_enrichment = enrichment.dup
-
5
new_enrichment.last_published_timestamp_utc = nil
-
5
new_enrichment.rolled_over!
-
5
new_course.enrichments << new_enrichment
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "csv"
-
-
1
module Exports
-
1
class AccreditedCourseList
-
1
def initialize(courses)
-
2
@data_for_export = format_courses(courses)
-
end
-
-
1
def data
-
2
CSV.generate(headers: data_for_export.first.keys, write_headers: true) do |csv|
-
2
data_for_export.each do |course_csv_row|
-
2
csv << course_csv_row
-
end
-
end
-
end
-
-
1
def filename
-
"courses-#{Time.zone.today}.csv"
-
end
-
-
1
private
-
-
1
attr_reader :data_for_export
-
-
1
def format_courses(courses)
-
2
courses
-
.map(&:decorate)
-
.flat_map do |c|
-
base_data = {
-
2
"Provider code" => c.provider.provider_code,
-
"Provider" => c.provider.provider_name,
-
"Course code" => c.course_code,
-
"Course" => c.name,
-
"Study mode" => c.study_mode&.humanize,
-
"Programme type" => c.program_type&.humanize,
-
"Qualification" => c.outcome,
-
"Status" => c.content_status&.to_s&.humanize,
-
"View on Find" => c.find_url,
-
"Applications open from" => I18n.l(c.applications_open_from&.to_date),
-
2
"Vacancies" => c.has_vacancies? ? "Yes" : "No",
-
}
-
2
if c.sites
-
2
base_data.merge({ "Campus Codes" => c.sites.pluck(:code).join(" ") })
-
else
-
base_data
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
4
module FeatureService
-
4
class << self
-
4
def require(feature_name)
-
4
unless enabled?(feature_name)
-
2
raise "Feature #{feature_name} is disabled"
-
end
-
-
2
true
-
end
-
-
4
def enabled?(feature_name)
-
18574
return false if Settings.features.blank?
-
-
18572
segments = feature_name.to_s.split(".")
-
-
39259
segments.reduce(Settings.features) { |config, segment| config[segment] }
-
end
-
end
-
end
-
2
class GenerateAndSendMagicLinkService
-
2
class << self
-
2
def call(user:)
-
3
new.call(user: user)
-
end
-
end
-
-
2
def call(user:)
-
3
generate_magic_link_token(user)
-
-
3
send_magic_link(user)
-
end
-
-
2
private
-
-
2
def generate_magic_link_token(user)
-
3
user.magic_link_token = SecureRandom.uuid
-
3
user.magic_link_token_sent_at = Time.now.utc
-
3
user.save!
-
end
-
-
2
def send_magic_link(user)
-
3
MagicLinkEmailMailer
-
.magic_link_email(user)
-
.deliver_later
-
end
-
end
-
2
class GeocoderService
-
2
def self.geocode(obj:, force: false)
-
5
return if obj.full_address.blank?
-
-
4
result = Geokit::Geocoders::GoogleGeocoder.geocode(obj.full_address, bias: "gb")
-
-
4
if result&.success == true
-
3
obj.latitude = result.latitude
-
3
obj.longitude = result.longitude
-
-
3
region_code = COUNTY_TO_REGION_CODE[result.district]
-
3
obj.region_code = region_code if region_code.present?
-
-
3
obj.save!(validate: !force)
-
end
-
end
-
end
-
3
module NotificationService
-
3
class CoursePublished
-
3
include ServicePattern
-
-
3
def initialize(course:)
-
8
@course = course
-
end
-
-
3
def call
-
8
return false unless notify_accredited_body?
-
4
return false unless course.in_current_cycle?
-
-
3
users.each do |user|
-
3
CoursePublishEmailMailer.course_publish_email(
-
course,
-
user,
-
).deliver_later
-
end
-
end
-
-
3
private
-
-
3
attr_reader :course
-
-
3
def users
-
3
User.course_publish_subscribers(course.accredited_body_code)
-
end
-
-
3
def notify_accredited_body?
-
8
return false if course.self_accredited?
-
7
return false unless course.findable?
-
-
4
true
-
end
-
end
-
end
-
2
module NotificationService
-
2
class CourseSitesUpdated
-
2
include ServicePattern
-
-
2
def initialize(course:, previous_site_names:, updated_site_names:)
-
7
@course = course
-
7
@previous_site_names = previous_site_names
-
7
@updated_site_names = updated_site_names
-
end
-
-
2
def call
-
7
return unless course_needs_to_notify?
-
-
4
users_to_notify.each do |user|
-
3
CourseSitesUpdateEmailMailer.course_sites_update_email(
-
course: course,
-
previous_site_names: previous_site_names,
-
updated_site_names: updated_site_names,
-
recipient: user,
-
).deliver_later
-
end
-
end
-
-
2
private
-
-
2
attr_reader :course, :previous_site_names, :updated_site_names
-
-
2
def users_to_notify
-
4
User.joins(:user_notifications).merge(
-
UserNotification.course_update_notification_requests(course.accredited_body_code),
-
)
-
end
-
-
2
def course_needs_to_notify?
-
7
course.findable? &&
-
!course.self_accredited? &&
-
course.in_current_cycle?
-
end
-
end
-
end
-
2
module NotificationService
-
2
class CourseSubjectsUpdated
-
2
include ServicePattern
-
-
2
def initialize(
-
course:,
-
previous_subject_names:,
-
previous_course_name:
-
)
-
9
@course = course
-
9
@previous_subject_names = previous_subject_names
-
9
@previous_course_name = previous_course_name
-
end
-
-
2
def call
-
9
return false unless notify_accredited_body?
-
4
return false unless course.in_current_cycle?
-
-
3
users.each do |user|
-
3
CourseSubjectsUpdatedEmailMailer.course_subjects_updated_email(
-
course: course,
-
previous_subject_names: previous_subject_names,
-
previous_course_name: previous_course_name,
-
recipient: user,
-
).deliver_later
-
end
-
end
-
-
2
private
-
-
2
attr_reader :course, :previous_subject_names, :previous_course_name
-
-
2
def users
-
3
User.course_update_subscribers(course.accredited_body_code)
-
end
-
-
2
def notify_accredited_body?
-
9
return false if course.self_accredited?
-
8
return false unless course.findable?
-
-
4
true
-
end
-
end
-
end
-
2
module NotificationService
-
2
class CourseUpdated
-
2
include ServicePattern
-
-
2
def initialize(course:)
-
6
@course = course
-
end
-
-
2
def call
-
6
return unless course_needs_to_notify?
-
-
3
notifiable_changes.each do |updated_attribute|
-
10
original_value, updated_value = course.saved_changes[updated_attribute]
-
-
10
users.each do |user|
-
20
CourseUpdateEmailMailer.course_update_email(
-
course: course,
-
attribute_name: updated_attribute,
-
original_value: original_value,
-
updated_value: updated_value,
-
recipient: user,
-
).deliver_later
-
end
-
end
-
end
-
-
2
private
-
-
2
attr_reader :course
-
-
2
def notifiable_changes
-
7
course.saved_changes.keys & course.update_notification_attributes
-
end
-
-
2
def users
-
10
User.course_update_subscribers(course.accredited_body_code)
-
end
-
-
2
def course_needs_to_notify?
-
6
course.findable? &&
-
!course.self_accredited? &&
-
notifiable_changes.any? &&
-
course.in_current_cycle?
-
end
-
end
-
end
-
2
module NotificationService
-
2
class CourseVacanciesUpdated
-
2
include ServicePattern
-
-
2
def initialize(course:, vacancy_statuses:)
-
16
@course = course
-
16
@vacancy_statuses = vacancy_statuses
-
end
-
-
2
def call
-
16
return false unless notify_accredited_body?
-
-
14
if all_vacancies_closed?
-
6
send_course_vacancies_updated_notification(vacancies_filled: true)
-
8
elsif all_vacancies_open?
-
5
send_course_vacancies_updated_notification(vacancies_filled: false)
-
else
-
3
send_course_vacancies_partially_updated_notification
-
end
-
end
-
-
2
private
-
-
2
attr_reader :course, :vacancy_statuses
-
-
2
def send_course_vacancies_updated_notification(vacancies_filled:)
-
11
users.each do |user|
-
8
CourseVacancies::UpdatedMailer.fully_updated(
-
course: course,
-
user: user,
-
datetime: DateTime.now,
-
vacancies_filled: vacancies_filled,
-
).deliver_later
-
end
-
end
-
-
2
def send_course_vacancies_partially_updated_notification
-
3
users.each do |user|
-
2
CourseVacancies::UpdatedMailer.partially_updated(
-
course: course,
-
user: user,
-
datetime: DateTime.now,
-
vacancies_opened: vacancies_opened,
-
vacancies_closed: vacancies_closed,
-
).deliver_later
-
end
-
end
-
-
2
def all_vacancies_open?
-
8
vacancy_statuses.all? do |vacancy_status|
-
11
vacancy_status[:status] == "full_time_vacancies" ||
-
vacancy_status[:status] == "both_full_time_and_part_time_vacancies" ||
-
vacancy_status[:status] == "part_time_vacancies"
-
end
-
end
-
-
2
def all_vacancies_closed?
-
33
vacancy_statuses.all? { |vacancy_status| vacancy_status[:status] == "no_vacancies" }
-
end
-
-
2
def vacancies_closed
-
2
vacancy_statuses
-
4
.select { |vacancy_status| vacancy_status[:status] == "no_vacancies" }
-
2
.map { |vacancy_status| SiteStatus.find(vacancy_status[:id]).site.location_name }
-
end
-
-
2
def vacancies_opened
-
2
vacancy_statuses
-
4
.select { |vacancy_status| vacancy_status[:status] == "full_time_vacancies" || vacancy_status[:status] == "part_time_vacancies" || vacancy_status[:status] == "both_full_time_and_part_time_vacancies" }
-
2
.map { |vacancy_status| SiteStatus.find(vacancy_status[:id]).site.location_name }
-
end
-
-
2
def users
-
14
User.course_publish_subscribers(course.accredited_body_code)
-
end
-
-
2
def notify_accredited_body?
-
16
return false if course.self_accredited?
-
15
return false unless course.in_current_cycle?
-
-
14
true
-
end
-
end
-
end
-
3
module NotificationService
-
3
class CourseWithdrawn
-
3
include ServicePattern
-
-
3
def initialize(course:)
-
6
@course = course
-
end
-
-
3
def call
-
6
return false unless notify_accredited_body?
-
5
return false unless course.in_current_cycle?
-
-
4
users.each do |user|
-
4
CourseWithdrawEmailMailer.course_withdraw_email(
-
course,
-
user,
-
DateTime.now,
-
).deliver_later
-
end
-
end
-
-
3
private
-
-
3
attr_reader :course
-
-
3
def users
-
4
User.course_publish_subscribers(course.accredited_body_code)
-
end
-
-
3
def notify_accredited_body?
-
6
return false if course.self_accredited?
-
-
5
true
-
end
-
end
-
end
-
class PerformanceDashboardService
-
include ActionView::Helpers::NumberHelper
-
-
include ServicePattern
-
-
attr_accessor :recruitment_cycle
-
-
def initialize(
-
recruitment_cycle: RecruitmentCycle.current_recruitment_cycle
-
)
-
@recruitment_cycle = recruitment_cycle
-
end
-
-
def call
-
self
-
end
-
-
def total_providers
-
number_with_delimiter(reporting[:providers][:total][:all])
-
end
-
-
def total_courses
-
number_with_delimiter(reporting[:courses][:total][:all_findable])
-
end
-
-
def total_users
-
number_with_delimiter(reporting[:publish][:users][:total][:all])
-
end
-
-
def total_allocations
-
number_with_delimiter(reporting[:allocations][:current][:total][:allocations])
-
end
-
-
def providers_published_courses
-
number_with_delimiter(reporting[:providers][:training_providers][:findable_total][:open])
-
end
-
-
def providers_unpublished_courses
-
number_with_delimiter(reporting[:providers][:training_providers][:findable_total][:closed])
-
end
-
-
def providers_accredited_bodies
-
number_with_delimiter(reporting[:providers][:training_providers][:accredited_body][:open][:accredited_body])
-
end
-
-
def courses_total_open
-
number_with_delimiter(reporting[:courses][:findable_total][:open])
-
end
-
-
def courses_total_closed
-
number_with_delimiter(reporting[:courses][:findable_total][:closed])
-
end
-
-
def courses_total_draft
-
number_with_delimiter(reporting[:courses][:total][:non_findable])
-
end
-
-
def allocations_requests(recruitment_cycle)
-
number_with_delimiter(reporting[:allocations][recruitment_cycle.to_sym][:total][:allocations])
-
end
-
-
def allocations_number_of_places(recruitment_cycle)
-
number_with_delimiter(reporting[:allocations][recruitment_cycle.to_sym][:total][:number_of_places])
-
end
-
-
def allocations_accredited_bodies(recruitment_cycle)
-
number_with_delimiter(reporting[:allocations][recruitment_cycle.to_sym][:total][:distinct_accredited_bodies])
-
end
-
-
def allocations_providers(recruitment_cycle)
-
number_with_delimiter(reporting[:allocations][recruitment_cycle.to_sym][:total][:distinct_providers])
-
end
-
-
def users_active
-
number_with_delimiter(reporting[:publish][:users][:total][:active_users])
-
end
-
-
def users_not_active
-
number_with_delimiter(reporting[:publish][:users][:total][:non_active_users])
-
end
-
-
def users_active_30_days
-
number_with_delimiter(reporting[:publish][:users][:recent_active_users])
-
end
-
-
def rollover_total
-
reporting[:rollover][:total]
-
end
-
-
def published_courses
-
number_with_delimiter(rollover_total[:published_courses])
-
end
-
-
def new_courses_published
-
number_with_delimiter(rollover_total[:new_courses_published])
-
end
-
-
def deleted_courses
-
number_with_delimiter(rollover_total[:deleted_courses])
-
end
-
-
def existing_courses_in_draft
-
number_with_delimiter(rollover_total[:existing_courses_in_draft])
-
end
-
-
def existing_courses_in_review
-
number_with_delimiter(rollover_total[:existing_courses_in_review])
-
end
-
-
private
-
-
def reporting
-
@reporting ||= StatisticService.reporting(recruitment_cycle: recruitment_cycle)
-
end
-
end
-
module ProviderEnrichments
-
class RolloverEnrichmentToProviderService
-
def execute(enrichment:, new_provider:)
-
new_enrichment = enrichment.dup
-
new_enrichment.last_published_at = nil
-
new_enrichment.provider = new_provider
-
new_enrichment.rolled_over!
-
end
-
end
-
end
-
3
class ProviderReportingService
-
3
def initialize(providers_scope: Provider)
-
11
@providers = providers_scope.distinct
-
-
11
@training_providers = @providers.where(id: Course.findable.select(:provider_id))
-
11
@open_providers = @providers.where(id: Course.with_vacancies.select(:provider_id))
-
-
11
@closed_providers = @training_providers.where.not(id: @open_providers)
-
end
-
-
3
class << self
-
3
def call(providers_scope:)
-
11
new(providers_scope: providers_scope).call
-
end
-
end
-
-
3
def call
-
{
-
11
total: {
-
all: @providers.count,
-
non_training_providers: @providers.count - @training_providers.count,
-
training_providers: @training_providers.count,
-
},
-
training_providers: {
-
findable_total: {
-
open: @open_providers.count,
-
closed: @closed_providers.count,
-
},
-
accredited_body: { **group_by_count(:accrediting_provider) },
-
provider_type: { **group_by_count(:provider_type) },
-
region_code: { **group_by_count(:region_code) },
-
},
-
}
-
end
-
-
3
private_class_method :new
-
-
3
private
-
-
3
def group_by_count(column)
-
33
open = @open_providers.group(column).count
-
33
closed = @closed_providers.group(column).count
-
-
{
-
33
open: Provider.send(column.to_s.pluralize).map { |key, _value|
-
187
x = {}
-
187
x[key.to_sym] = open[key] || 0
-
187
x
-
} .reduce({}, :merge),
-
closed: Provider.send(column.to_s.pluralize).map { |key, _value|
-
187
x = {}
-
187
x[key.to_sym] = closed[key] || 0
-
187
x
-
} .reduce({}, :merge),
-
}
-
end
-
end
-
1
module Providers
-
1
class CopyToRecruitmentCycleService
-
1
attr :logger
-
-
1
def initialize(copy_course_to_provider_service:, copy_site_to_provider_service:, logger: nil)
-
14
@copy_course_to_provider_service = copy_course_to_provider_service
-
14
@copy_site_to_provider_service = copy_site_to_provider_service
-
14
@logger = logger || Logger.new("/dev/null")
-
end
-
-
1
def execute(provider:, new_recruitment_cycle:, force: false)
-
14
providers_count = 0
-
14
sites_count = 0
-
14
courses_count = 0
-
-
14
if provider.rollable? || force
-
14
ActiveRecord::Base.transaction do
-
14
rolled_over_provider = new_recruitment_cycle.providers.find_by(provider_code: provider.provider_code)
-
14
if rolled_over_provider.nil?
-
11
providers_count = 1
-
11
rolled_over_provider = provider.dup
-
11
rolled_over_provider.organisations << provider.organisations
-
11
rolled_over_provider.ucas_preferences = provider.ucas_preferences.dup
-
11
rolled_over_provider.contacts << provider.contacts.map(&:dup)
-
11
rolled_over_provider.recruitment_cycle = new_recruitment_cycle
-
11
rolled_over_provider.skip_geocoding = true
-
11
rolled_over_provider.users << provider.users
-
11
rolled_over_provider.save!
-
end
-
-
# Order is important here. Sites should be copied over before courses
-
# so that courses can link up to the correct sites in the new provider.
-
14
sites_count = copy_sites_to_new_provider(provider, rolled_over_provider)
-
14
courses_count = copy_courses_to_new_provider(provider, rolled_over_provider)
-
end
-
end
-
-
{
-
13
providers: providers_count,
-
sites: sites_count,
-
courses: courses_count,
-
}
-
end
-
-
1
private
-
-
1
def copy_courses_to_new_provider(provider, new_provider)
-
14
courses_count = 0
-
-
14
provider.courses.each do |course|
-
14
new_course = @copy_course_to_provider_service.execute(course: course, new_provider: new_provider)
-
13
courses_count += 1 if new_course.present?
-
rescue Exception # rubocop: disable Lint/RescueException
-
1
logger&.fatal "error trying to copy course #{course.course_code}"
-
1
raise
-
end
-
-
13
courses_count
-
end
-
-
1
def copy_sites_to_new_provider(provider, new_provider)
-
14
sites_count = 0
-
-
14
provider.sites.each do |site|
-
14
new_site = @copy_site_to_provider_service.execute(site: site, new_provider: new_provider)
-
14
sites_count += 1 if new_site.present?
-
end
-
-
14
sites_count
-
end
-
end
-
end
-
2
module Providers
-
2
class CreateFakeProviderService
-
2
attr_reader :errors
-
-
DEFAULT_PROVIDER_ATTRIBUTES = {
-
2
address1: "1 Test Street",
-
address3: "Town",
-
address4: "County",
-
postcode: "M1 1JG",
-
region_code: "north_west",
-
ukprn: "12345678",
-
}.freeze
-
-
2
def initialize(recruitment_cycle:, provider_name:, provider_code:, provider_type:, is_accredited_body:)
-
13
raise "Can only be run in sandbox or development" unless Rails.env.sandbox? || Rails.env.development? || Rails.env.test?
-
-
13
@recruitment_cycle = recruitment_cycle
-
13
@provider_name = provider_name
-
13
@provider_code = provider_code
-
13
@provider_type = provider_type
-
13
@is_accredited_body = is_accredited_body
-
-
13
@errors = []
-
end
-
-
2
def execute
-
13
return false if provider_already_exists?
-
11
return false if attempting_to_make_lead_school_accredited_body?
-
-
10
provider = @recruitment_cycle.providers.build({
-
provider_name: @provider_name,
-
provider_code: @provider_code,
-
provider_type: @provider_type,
-
10
accrediting_provider: @is_accredited_body ? "accredited_body" : "not_an_accredited_body",
-
}.merge(DEFAULT_PROVIDER_ATTRIBUTES))
-
-
10
organisation = Organisation.new(name: @provider_name)
-
10
organisation.providers << provider
-
10
organisation.save!
-
-
10
provider.sites.create!(
-
location_name: "Site 1",
-
address1: provider.address1,
-
address2: provider.address2,
-
address3: provider.address3,
-
address4: provider.address4,
-
postcode: provider.postcode,
-
region_code: provider.region_code,
-
urn: Faker::Number.number(digits: [5, 6].sample),
-
)
-
-
10
true
-
end
-
-
2
private
-
-
2
def provider_already_exists?
-
13
if @recruitment_cycle.providers.exists?(provider_code: @provider_code)
-
2
errors << "Provider #{@provider_name} (#{@provider_code}) already exists."
-
2
true
-
else
-
11
false
-
end
-
end
-
-
2
def attempting_to_make_lead_school_accredited_body?
-
11
if @provider_type == "lead_school" && @is_accredited_body
-
1
errors << "Provider #{@provider_name} (#{@provider_code}) cannot be both a lead school and an accredited body."
-
end
-
end
-
end
-
end
-
4
module Providers
-
4
class GenerateCourseCodeService
-
4
def execute
-
7
"#{valid_letters.sample}#{valid_number}"
-
end
-
-
4
private
-
-
4
def valid_letters
-
10
("A".."Z").to_a - %w[O I]
-
end
-
-
4
def valid_number
-
7
[*0..999].sample.to_s.rjust(3, "0")
-
end
-
end
-
end
-
3
module Providers
-
3
class GenerateUniqueCourseCodeService
-
3
def initialize(generate_course_code_service:)
-
9
@generate_course_code_service = generate_course_code_service
-
end
-
-
3
def execute(existing_codes:)
-
9
code = nil
-
-
9
code = @generate_course_code_service.execute while code.nil? || code.in?(existing_codes)
-
-
9
code
-
end
-
end
-
end
-
2
module Providers
-
2
class VisaSponsorshipService
-
2
VISA_SPONSORSHIP_INTRODUCED_IN = 2022
-
-
2
def initialize(provider)
-
8
@provider = provider
-
end
-
-
2
def visa_sponsorship_enabled?
-
8
@provider.recruitment_cycle_year.to_i >= VISA_SPONSORSHIP_INTRODUCED_IN
-
end
-
end
-
end
-
4
module Publish
-
4
class CourseCreationStepService
-
4
def execute(current_step:, course:)
-
162
workflow_steps = get_workflow_steps(course)
-
{
-
162
next: get_next_step(workflow_steps, current_step),
-
previous: get_previous_step(workflow_steps, current_step),
-
}
-
end
-
-
4
def get_next_step(steps, current_step)
-
162
next_step_index = steps.find_index(current_step).next
-
162
steps[next_step_index]
-
end
-
-
4
def get_previous_step(steps, current_step)
-
162
previous_step_index = steps.find_index(current_step).pred
-
162
steps[previous_step_index]
-
end
-
-
4
def get_workflow_steps(course)
-
162
workflows_for_2022_cycle_onwards(course)
-
end
-
-
4
private
-
-
4
def workflows_for_2022_cycle_onwards(course)
-
162
if course.is_further_education?
-
3
new_further_education_workflow_steps
-
159
elsif course.is_uni_or_scitt?
-
39
new_uni_or_scitt_workflow_steps
-
120
elsif course.is_school_direct?
-
120
new_school_direct_workflow_steps
-
end
-
end
-
-
4
def workflows_for_current_cycle(course)
-
if course.is_further_education?
-
further_education_workflow_steps
-
elsif course.is_uni_or_scitt?
-
uni_or_scitt_workflow_steps
-
elsif course.is_school_direct?
-
school_direct_workflow_steps
-
end
-
end
-
-
4
def school_direct_workflow_steps
-
%i[
-
courses_list
-
level
-
subjects
-
modern_languages
-
age_range
-
outcome
-
fee_or_salary
-
full_or_part_time
-
location
-
accredited_body
-
entry_requirements
-
applications_open
-
start_date
-
confirmation
-
]
-
end
-
-
4
def uni_or_scitt_workflow_steps
-
%i[
-
courses_list
-
level
-
subjects
-
modern_languages
-
age_range
-
outcome
-
apprenticeship
-
full_or_part_time
-
location
-
entry_requirements
-
applications_open
-
start_date
-
confirmation
-
]
-
end
-
-
4
def further_education_workflow_steps
-
3
%i[
-
courses_list
-
level
-
outcome
-
full_or_part_time
-
location
-
applications_open
-
start_date
-
confirmation
-
]
-
end
-
-
4
def new_school_direct_workflow_steps
-
120
%i[
-
courses_list
-
level
-
subjects
-
modern_languages
-
age_range
-
outcome
-
fee_or_salary
-
full_or_part_time
-
location
-
accredited_body
-
applications_open
-
start_date
-
confirmation
-
]
-
end
-
-
4
def new_uni_or_scitt_workflow_steps
-
39
%i[
-
courses_list
-
level
-
subjects
-
modern_languages
-
age_range
-
outcome
-
apprenticeship
-
full_or_part_time
-
location
-
applications_open
-
start_date
-
confirmation
-
]
-
end
-
-
4
def new_further_education_workflow_steps
-
3
further_education_workflow_steps
-
end
-
end
-
end
-
2
class PublishReportingService
-
2
def initialize(recruitment_cycle_scope: RecruitmentCycle)
-
11
@courses = recruitment_cycle_scope.courses
-
11
@providers = recruitment_cycle_scope.providers
-
end
-
-
2
class << self
-
2
def call(recruitment_cycle_scope:)
-
11
new(recruitment_cycle_scope: recruitment_cycle_scope).call
-
end
-
end
-
-
2
def call
-
{
-
11
users: user_breakdown,
-
providers: provider_breakdown,
-
courses: course_breakdown,
-
}
-
end
-
-
2
private_class_method :new
-
-
2
private
-
-
2
def days_ago
-
33
@days_ago ||= 30.days.ago
-
end
-
-
2
def active_users
-
22
@active_users ||= User.active
-
end
-
-
2
def recent_active_users
-
33
@recent_active_users ||= active_users.last_login_since(days_ago)
-
end
-
-
2
def providers_with_recent_active_users_distinct_count
-
22
@providers_with_recent_active_users_distinct_count ||= @providers
-
.joins(:users)
-
.merge(recent_active_users)
-
.distinct
-
.count
-
end
-
-
2
def recent_active_user_count_by_provider
-
11
@recent_active_user_count_by_provider ||= recent_active_users
-
.joins(:providers) # Results include a user entry for _each_ matching provider
-
.merge(@providers) # Limit our scope to the current recruitment Cycle
-
.group("provider_id")
-
.count # Count the users for each provider
-
end
-
-
2
def grouped_providers_with_x_active_users
-
56
@grouped_providers_with_x_active_users ||= recent_active_user_count_by_provider
-
.group_by(&:second) # Group the results by the number of users they have
-
.transform_values(&:count) # Count the results
-
end
-
-
2
def with_more_than_5_recent_active_users
-
12
grouped_providers_with_x_active_users.keys.excluding(4, 3, 2, 1).sum { |k| grouped_providers_with_x_active_users[k] || 0 }
-
end
-
-
2
def user_count
-
22
@user_count ||= User.count
-
end
-
-
2
def active_users_count
-
22
@active_users_count ||= active_users.count
-
end
-
-
2
def recent_active_users_count
-
11
@recent_active_users_count ||= recent_active_users.count
-
end
-
-
2
def provider_count
-
22
@provider_count ||= @providers.count
-
end
-
-
2
def user_breakdown
-
{
-
11
total: {
-
all: user_count,
-
active_users: active_users_count,
-
non_active_users: user_count - active_users_count,
-
},
-
recent_active_users: recent_active_users_count,
-
}
-
end
-
-
2
def provider_breakdown
-
{
-
11
total: {
-
all: provider_count,
-
11
providers_with_non_active_users: (provider_count - providers_with_recent_active_users_distinct_count),
-
providers_with_recent_active_users: providers_with_recent_active_users_distinct_count,
-
},
-
-
with_1_recent_active_users: grouped_providers_with_x_active_users[1] || 0,
-
with_2_recent_active_users: grouped_providers_with_x_active_users[2] || 0,
-
with_3_recent_active_users: grouped_providers_with_x_active_users[3] || 0,
-
with_4_recent_active_users: grouped_providers_with_x_active_users[4] || 0,
-
with_more_than_5_recent_active_users: with_more_than_5_recent_active_users,
-
}
-
end
-
-
2
def course_breakdown
-
11
courses_changed_recently = @courses.changed_at_since(days_ago)
-
-
11
findable_courses = courses_changed_recently.findable.distinct
-
11
open_courses = findable_courses.with_vacancies
-
11
closed_courses = findable_courses.where.not(id: open_courses)
-
-
11
courses_changed_recently_count = courses_changed_recently.count
-
11
findable_courses_count = findable_courses.count
-
-
{
-
11
total_updated_recently: courses_changed_recently_count,
-
updated_non_findable_recently: courses_changed_recently_count - findable_courses_count,
-
-
updated_findable_recently: findable_courses_count,
-
updated_open_courses_recently: open_courses.count,
-
updated_closed_courses_recently: closed_courses.count,
-
-
created_recently: @courses.created_at_since(days_ago).count,
-
}
-
end
-
end
-
2
class RecordFirstLoginService
-
2
def execute(current_user:)
-
3
return unless current_user.first_login_date_utc.nil?
-
-
1
current_user.update(first_login_date_utc: Time.now.utc)
-
end
-
end
-
1
class RecruitmentCycleCreationService
-
1
include ServicePattern
-
-
1
def initialize(year:, application_start_date:, application_end_date:, summary: false)
-
1
@year = year
-
1
@application_start_date = application_start_date
-
1
@application_end_date = application_end_date
-
1
@summary = summary
-
end
-
-
1
def call
-
1
RecruitmentCycle.create!(year: @year, application_start_date: @application_start_date, application_end_date: @application_end_date)
-
-
1
if @summary
-
Rails.logger.info { "The new RecruitmentCycle has been successfully created for:\n\nyear: '#{@year}'\napplication_start_date: '#{@application_start_date}'\napplication_end_date: '#{@application_end_date}'\n\n" }
-
end
-
end
-
end
-
2
class RolloverReportingService
-
2
def initialize
-
16
@rollover_scope = RecruitmentCycle.current_recruitment_cycle
-
end
-
-
2
class << self
-
2
delegate :call, to: :new
-
end
-
-
2
def call
-
16
if @rollover_scope.next.blank?
-
11
{ total: {
-
published_courses: 0,
-
new_courses_published: 0,
-
deleted_courses: 0,
-
existing_courses_in_draft: 0,
-
existing_courses_in_review: 0,
-
} }
-
else
-
{
-
5
total: {
-
published_courses: published_courses_count,
-
new_courses_published: new_courses_published_count,
-
deleted_courses: deleted_courses_count,
-
existing_courses_in_draft: existing_courses_in_draft_count,
-
existing_courses_in_review: existing_courses_in_review,
-
},
-
}
-
end
-
end
-
-
2
private_class_method :new
-
-
2
private
-
-
2
def next_findable
-
25
@rollover_scope.next.courses.findable
-
end
-
-
2
def published_courses
-
20
next_findable
-
end
-
-
2
def published_courses_count
-
5
published_courses.distinct.count
-
end
-
-
2
def new_courses_published
-
5
next_findable.distinct.created_at_since(RecruitmentCycle.next.created_at + 1.day)
-
end
-
-
2
def new_courses_published_count
-
5
new_courses_published.count
-
end
-
-
2
def deleted_courses
-
10
Course.discarded.where(provider: Provider.where(recruitment_cycle: RecruitmentCycle.next))
-
end
-
-
2
def deleted_courses_count
-
5
deleted_courses.count
-
end
-
-
2
def existing_courses_in_draft
-
10
@rollover_scope.next.courses.changed_since(RecruitmentCycle.next.created_at + 1.day).where.not(id: published_courses)
-
end
-
-
2
def existing_courses_in_draft_count
-
5
existing_courses_in_draft.distinct.count
-
end
-
-
2
def existing_courses_in_review
-
5
@rollover_scope.next.courses.where.not(id: published_courses).where.not(id: deleted_courses).where.not(id: existing_courses_in_draft).count
-
end
-
end
-
1
class RolloverService
-
1
include ServicePattern
-
-
1
def initialize(provider_codes:, force: false)
-
6
@provider_codes = provider_codes
-
6
@force = force
-
end
-
-
1
def call
-
6
total_counts = { providers: 0, sites: 0, courses: 0 }
-
-
6
total_bm = Benchmark.measure do
-
15
providers.each { |provider| rollover(provider, total_counts) }
-
end
-
-
6
Rails.logger.info "Rollover done: " \
-
"#{total_counts[:providers]} providers, " \
-
"#{total_counts[:sites]} sites, " \
-
"#{total_counts[:courses]} courses " +
-
format("in %.3f seconds", total_bm.real)
-
end
-
-
1
private
-
-
1
attr_reader :provider_codes, :force
-
-
1
def rollover(provider, total_counts)
-
9
Rails.logger.info { "Copying provider #{provider.provider_code}: " }
-
9
counts = nil
-
-
9
bm = Benchmark.measure do
-
9
Provider.connection.transaction do
-
9
copy_courses_to_provider_service = Courses::CopyToProviderService.new(
-
sites_copy_to_course: Sites::CopyToCourseService.new,
-
enrichments_copy_to_course: Enrichments::CopyToCourseService.new,
-
)
-
-
9
copy_provider_to_recruitment_cycle = Providers::CopyToRecruitmentCycleService.new(
-
copy_course_to_provider_service: copy_courses_to_provider_service,
-
copy_site_to_provider_service: Sites::CopyToProviderService.new,
-
logger: Logger.new($stdout),
-
)
-
-
9
counts = copy_provider_to_recruitment_cycle.execute(
-
provider: provider, new_recruitment_cycle: new_recruitment_cycle, force: force,
-
)
-
end
-
end
-
-
9
Rails.logger.info "provider #{counts[:providers].zero? ? 'skipped' : 'copied'}, " \
-
"#{counts[:sites]} sites copied, " \
-
"#{counts[:courses]} courses copied " +
-
format("in %.3f seconds", bm.real)
-
-
36
total_counts.merge!(counts) { |_, total, count| total + count }
-
end
-
-
1
def new_recruitment_cycle
-
9
@new_recruitment_cycle ||= RecruitmentCycle.next_recruitment_cycle
-
end
-
-
1
def providers
-
6
@providers ||= if provider_codes.any?
-
3
RecruitmentCycle.current_recruitment_cycle
-
.providers.where(provider_code: provider_codes.to_a.map(&:upcase))
-
else
-
3
RecruitmentCycle.current_recruitment_cycle.providers
-
end
-
end
-
end
-
1
class SendWelcomeEmailService
-
1
class MissingFirstNameError < StandardError; end
-
1
class << self
-
1
def call(current_user:)
-
18
new.call(current_user: current_user)
-
end
-
end
-
-
1
def call(current_user:)
-
18
return if current_user.welcome_email_date_utc
-
-
# Getting visibility on the missing personalisation first_name issue
-
14
raise MissingFirstNameError, "This user (user.id: #{current_user.id}) does not have a first name." if current_user.first_name.blank?
-
-
8
WelcomeEmailMailer
-
.send_welcome_email(first_name: current_user.first_name, email: current_user.email)
-
.deliver_later
-
-
8
current_user.update(
-
welcome_email_date_utc: Time.now.utc,
-
)
-
rescue MissingFirstNameError => e
-
6
Sentry.capture_exception(e)
-
end
-
end
-
1
class SerializeCourseService
-
1
def initialize(serializers_service: CourseSerializersService.new, renderer: JSONAPI::Serializable::Renderer.new)
-
3
@serializers_service = serializers_service
-
3
@renderer = renderer
-
end
-
-
1
def execute(object:)
-
3
{ serialized: @renderer.render(object, class: @serializers_service.execute) }
-
end
-
end
-
1
require "dry/container"
-
-
1
class ServiceContainer
-
1
def initialize
-
1
@services = Dry::Container.new
-
1
register_services
-
end
-
-
1
def define(namespace, name, &block)
-
6
@services.namespace(namespace) do
-
6
register(name, block)
-
end
-
end
-
-
1
def get(namespace, service_name)
-
1
@services.resolve("#{namespace}.#{service_name}")
-
end
-
-
1
private
-
-
1
def register_services
-
1
define(:courses, :copy_to_provider) do
-
Courses::CopyToProviderService.new(
-
sites_copy_to_course: get(:sites, :copy_to_course),
-
enrichments_copy_to_course: get(:course_enrichments, :copy_to_course),
-
)
-
end
-
-
1
define(:sites, :copy_to_course) do
-
Sites::CopyToCourseService.new
-
end
-
-
1
define(:sites, :copy_to_provider) do
-
Sites::CopyToProviderService.new
-
end
-
-
1
define(:course_enrichments, :copy_to_course) do
-
Enrichments::CopyToCourseService.new
-
end
-
-
1
define(:providers, :copy_to_recruitment_cycle) do
-
Providers::CopyToRecruitmentCycleService.new(
-
copy_course_to_provider_service: get(:courses, :copy_to_provider),
-
copy_site_to_provider_service: get(:sites, :copy_to_provider),
-
)
-
end
-
end
-
end
-
4
module ServicePattern
-
4
def self.included(base)
-
42
base.extend ClassMethods
-
42
base.class_eval do
-
42
private_class_method :new
-
end
-
end
-
-
4
def call
-
raise NotImplementedError("#call must be implemented")
-
end
-
-
4
module ClassMethods
-
4
def call(**args)
-
753
return new.call if args.empty?
-
-
690
new(args).call
-
end
-
end
-
end
-
3
module Sites
-
3
class CodeGenerator
-
3
include ServicePattern
-
-
3
def initialize(provider:)
-
20
@provider = provider
-
end
-
-
3
def call
-
20
ucas_style_code.presence || highest_site_code.next
-
end
-
-
3
private
-
-
3
attr_reader :provider
-
-
3
def highest_site_code
-
5
return "Z" if existing_sequential_codes.blank?
-
-
3
existing_sequential_codes.max
-
end
-
-
3
def existing_sequential_codes
-
8
(existing_site_codes - Site::POSSIBLE_CODES).compact
-
end
-
-
3
def existing_site_codes
-
48
@existing_site_codes ||= provider.sites.pluck(:code)
-
end
-
-
3
def unassigned_ucas_style_site_codes
-
40
Site::POSSIBLE_CODES - existing_site_codes
-
end
-
-
3
def ucas_style_code
-
20
@ucas_style_code ||= begin
-
20
available_desirable_codes = unassigned_ucas_style_site_codes & Site::DESIRABLE_CODES
-
20
available_undesirable_codes = unassigned_ucas_style_site_codes & Site::EASILY_CONFUSED_CODES
-
-
20
available_desirable_codes.sample || available_undesirable_codes.sample
-
end
-
end
-
end
-
end
-
2
module Sites
-
2
class CopyToCourseService
-
2
def execute(new_site:, new_course:)
-
4
new_vac_status = SiteStatus.default_vac_status_given(
-
study_mode: new_course.study_mode,
-
)
-
-
4
new_course.site_statuses.create(
-
site: new_site,
-
vac_status: new_vac_status,
-
status: :new_status,
-
)
-
end
-
end
-
end
-
2
module Sites
-
2
class CopyToProviderService
-
2
def execute(site:, new_provider:)
-
4
new_site = new_provider.sites.find_by(code: site.code)
-
-
4
return if new_site.present?
-
-
3
new_site = site.dup
-
3
new_site.provider_id = new_provider.id
-
3
new_site.skip_geocoding = true
-
3
new_site.uuid = SecureRandom.uuid
-
3
new_site.save(validate: false)
-
3
new_provider.reload
-
-
3
new_site
-
end
-
end
-
end
-
3
class StatisticService
-
3
def self.reporting(recruitment_cycle:)
-
{
-
12
providers: ProviderReportingService.call(providers_scope: recruitment_cycle.providers),
-
courses: CourseReportingService.call(courses_scope: recruitment_cycle.courses),
-
publish: PublishReportingService.call(recruitment_cycle_scope: recruitment_cycle),
-
allocations: AllocationReportingService.call(recruitment_cycle_scope: RecruitmentCycle.find_by(year: Settings.allocation_cycle_year)),
-
rollover: RolloverReportingService.call,
-
}
-
end
-
-
3
def self.save(recruitment_cycle: RecruitmentCycle.current_recruitment_cycle)
-
6
Statistic.create!(json_data: reporting(recruitment_cycle: recruitment_cycle))
-
end
-
end
-
4
module Subjects
-
4
class CreatorService
-
4
def initialize(primary_subject: ::PrimarySubject,
-
secondary_subject: ::SecondarySubject,
-
further_education_subject: ::FurtherEducationSubject,
-
modern_languages_subject: ::ModernLanguagesSubject,
-
discontinued_subject: ::DiscontinuedSubject)
-
316
@primary_subject = primary_subject
-
316
@secondary_subject = secondary_subject
-
316
@further_education_subject = further_education_subject
-
316
@modern_languages_subject = modern_languages_subject
-
316
@discontinued_subject = discontinued_subject
-
end
-
-
4
def execute
-
primary = [
-
316
{ subject_name: "Primary", subject_code: "00" },
-
{ subject_name: "Primary with English", subject_code: "01" },
-
{ subject_name: "Primary with geography and history", subject_code: "02" },
-
{ subject_name: "Primary with mathematics", subject_code: "03" },
-
{ subject_name: "Primary with modern languages", subject_code: "04" },
-
{ subject_name: "Primary with physical education", subject_code: "06" },
-
{ subject_name: "Primary with science", subject_code: "07" },
-
]
-
-
secondary = [
-
316
{ subject_name: "Art and design", subject_code: "W1" },
-
{ subject_name: "Science", subject_code: "F0" },
-
{ subject_name: "Biology", subject_code: "C1" },
-
{ subject_name: "Business studies", subject_code: "08" },
-
{ subject_name: "Chemistry", subject_code: "F1" },
-
{ subject_name: "Citizenship", subject_code: "09" },
-
{ subject_name: "Classics", subject_code: "Q8" },
-
{ subject_name: "Communication and media studies", subject_code: "P3" },
-
{ subject_name: "Computing", subject_code: "11" },
-
{ subject_name: "Dance", subject_code: "12" },
-
{ subject_name: "Design and technology", subject_code: "DT" },
-
{ subject_name: "Drama", subject_code: "13" },
-
{ subject_name: "Economics", subject_code: "L1" },
-
{ subject_name: "English", subject_code: "Q3" },
-
{ subject_name: "Geography", subject_code: "F8" },
-
{ subject_name: "Health and social care", subject_code: "L5" },
-
{ subject_name: "History", subject_code: "V1" },
-
{ subject_name: "Mathematics", subject_code: "G1" },
-
{ subject_name: "Music", subject_code: "W3" },
-
{ subject_name: "Philosophy", subject_code: "P1" },
-
{ subject_name: "Physical education", subject_code: "C6" },
-
{ subject_name: "Physics", subject_code: "F3" },
-
{ subject_name: "Psychology", subject_code: "C8" },
-
{ subject_name: "Religious education", subject_code: "V6" },
-
{ subject_name: "Social sciences", subject_code: "14" },
-
# NOTE: no subject_code for 'Modern Languages' because this is just a stub used to trigger
-
# selection of actual entries from `modern_languages` list
-
{ subject_name: "Modern Languages", subject_code: nil },
-
-
# NOTE: These subject_code are contrived.
-
{ subject_name: "Latin", subject_code: "A0" },
-
{ subject_name: "Ancient Greek", subject_code: "A1" },
-
{ subject_name: "Ancient Hebrew", subject_code: "A2" },
-
]
-
-
modern_languages = [
-
316
{ subject_name: "French", subject_code: "15" },
-
{ subject_name: "English as a second or other language", subject_code: "16" },
-
{ subject_name: "German", subject_code: "17" },
-
{ subject_name: "Italian", subject_code: "18" },
-
{ subject_name: "Japanese", subject_code: "19" },
-
{ subject_name: "Mandarin", subject_code: "20" },
-
{ subject_name: "Russian", subject_code: "21" },
-
{ subject_name: "Spanish", subject_code: "22" },
-
{ subject_name: "Modern languages (other)", subject_code: "24" },
-
]
-
-
further_education = [
-
316
{ subject_name: "Further education", subject_code: "41" },
-
]
-
-
# old 2019 DfE subjects
-
discontinued = [
-
316
{ subject_name: "Humanities" },
-
{ subject_name: "Balanced Science" },
-
]
-
-
316
primary.each do |subject|
-
2212
@primary_subject.find_or_create_by!(subject_name: subject[:subject_name], subject_code: subject[:subject_code])
-
end
-
-
316
secondary.each do |subject|
-
9164
@secondary_subject.find_or_create_by!(subject_name: subject[:subject_name], subject_code: subject[:subject_code])
-
end
-
-
316
modern_languages.each do |subject|
-
2844
@modern_languages_subject.find_or_create_by!(subject_name: subject[:subject_name], subject_code: subject[:subject_code])
-
end
-
-
316
further_education.each do |subject|
-
316
@further_education_subject.find_or_create_by!(subject_name: subject[:subject_name], subject_code: subject[:subject_code])
-
end
-
-
316
discontinued.each do |subject|
-
632
@discontinued_subject.find_or_create_by!(subject_name: subject[:subject_name])
-
end
-
end
-
end
-
end
-
4
module Subjects
-
4
class FinancialIncentiveCreatorService
-
4
def initialize(year:, subject: Subject, financial_incentive: FinancialIncentive)
-
317
@subject = subject
-
317
@financial_incentive = financial_incentive
-
317
@year = year
-
end
-
-
4
def subject_and_financial_incentives
-
subject_and_financial_incentives = {
-
317
2022 => {
-
["Latin", "Ancient Greek", "Ancient Hebrew"] => {
-
bursary_amount: "15000",
-
},
-
},
-
2021 => {
-
%w[Biology] => {
-
bursary_amount: "7000",
-
},
-
%w[Chemistry Computing Mathematics Physics] => {
-
scholarship: "26000",
-
bursary_amount: "24000",
-
},
-
%w[Classics] => {
-
bursary_amount: "10000",
-
},
-
["French", "German", "Italian", "Japanese", "Mandarin", "Modern languages (other)", "Russian", "Spanish"] => { bursary_amount: "10000" },
-
},
-
2020 => {
-
["Primary with mathematics"] => {
-
bursary_amount: "6000",
-
},
-
%w[Biology Classics] => {
-
bursary_amount: "26000",
-
},
-
%w[French German Spanish] => {
-
bursary_amount: "26000",
-
scholarship: "28000",
-
early_career_payments: "2000",
-
},
-
%w[Computing] => {
-
bursary_amount: "26000",
-
scholarship: "28000",
-
},
-
%w[Geography] => {
-
bursary_amount: "15000",
-
scholarship: "17000",
-
},
-
["Italian", "Japanese", "Mandarin", "Russian", "Modern languages (other)"] => {
-
bursary_amount: "26000",
-
early_career_payments: "2000",
-
},
-
["Art and design", "Business studies", "History", "Music", "Religious education"] => {
-
bursary_amount: "9000",
-
},
-
%w[English] => {
-
bursary_amount: "12000",
-
},
-
["Design and technology"] => {
-
bursary_amount: "15000",
-
},
-
%w[Chemistry Mathematics Physics] => {
-
bursary_amount: "26000",
-
scholarship: "28000",
-
early_career_payments: "2000",
-
},
-
},
-
}
-
317
subject_and_financial_incentives[@year] || {}
-
end
-
-
4
def execute
-
317
subject_and_financial_incentives.each do |subject_name, financial_incentive_attributes|
-
1271
@subject.where(subject_name: subject_name).each do |subject|
-
4443
@financial_incentive.find_or_create_by(subject: subject, **financial_incentive_attributes)
-
end
-
end
-
end
-
end
-
end
-
4
module Subjects
-
4
class FinancialIncentiveSetSubjectKnowledgeEnhancementCourseAvailableService
-
4
def initialize(year:, financial_incentive: FinancialIncentive)
-
316
@financial_incentive = financial_incentive
-
316
@year = year
-
end
-
-
4
def subject_with_subject_knowledge_enhancement_course_available
-
subject_with_subject_knowledge_enhancement_course_available = {
-
316
2020 => ["Primary with mathematics", "Biology", "Computing", "English", "Design and technology", "Geography", "Chemistry", "Mathematics", "Physics", "French", "German", "Spanish", "Italian", "Japanese", "Mandarin", "Russian", "Modern languages (other)", "Religious education"],
-
}
-
316
subject_with_subject_knowledge_enhancement_course_available[@year]
-
end
-
-
4
def execute
-
316
financial_incentives = @financial_incentive.joins(:subject).where(subject: { subject_name: subject_with_subject_knowledge_enhancement_course_available })
-
316
financial_incentives.update_all(subject_knowledge_enhancement_course_available: true)
-
end
-
end
-
end
-
4
module Subjects
-
4
class SubjectAreaCreatorService
-
4
def initialize(subject_area: SubjectArea)
-
316
@subject_area = subject_area
-
end
-
-
4
def execute
-
316
@subject_area.find_or_create_by!(typename: "PrimarySubject", name: "Primary")
-
316
@subject_area.find_or_create_by!(typename: "SecondarySubject", name: "Secondary")
-
316
@subject_area.find_or_create_by!(typename: "ModernLanguagesSubject", name: "Secondary: Modern languages")
-
316
@subject_area.find_or_create_by!(typename: "FurtherEducationSubject", name: "Further education")
-
316
@subject_area.find_or_create_by!(typename: "DiscontinuedSubject", name: "Discontinued")
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Support
-
1
module DataExports
-
1
class Base
-
1
def to_csv(data_for_export: data)
-
1
require "csv"
-
1
header_row = data_for_export.first.keys
-
1
::CSV.generate(headers: true) do |rows|
-
1
rows << header_row
-
1
data_for_export.map(&:values).each do |value|
-
2
rows << value
-
end
-
end
-
end
-
-
1
def filename
-
"#{self.class.name.parameterize}_#{Time.zone.now.strftime('%Y-%m-%d_%H-%M_%S')}.csv"
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Support
-
1
module DataExports
-
1
class DataExport
-
1
class << self
-
1
def all
-
[
-
3
DataExports::UsersExport,
-
].map(&:new)
-
end
-
-
1
def find(type)
-
2
all.detect { |d| d.type == type }
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Support
-
1
module DataExports
-
1
class UsersExport < Base
-
1
def type
-
3
"users"
-
end
-
-
1
def data
-
1
result = []
-
1
RecruitmentCycle.current.providers.find_each do |provider|
-
1
provider.users.find_each do |user|
-
2
result << user_data(user, provider)
-
end
-
end
-
1
result
-
end
-
-
1
private
-
-
1
def user_data(user, provider)
-
{
-
3
user_id: user.id,
-
provider_code: provider.provider_code,
-
provider_name: provider.provider_name,
-
provider_type: provider.provider_type,
-
first_name: user.first_name,
-
last_name: user.last_name,
-
email_address: user.email,
-
first_login_at: user.first_login_date_utc,
-
last_login_at: user.last_login_date_utc,
-
sign_in_user_id: user.sign_in_user_id,
-
}
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
4
module Support
-
4
class Filter
-
4
include ServicePattern
-
-
4
def initialize(model_data_scope:, filter_params:)
-
50
@model_data_scope = model_data_scope
-
50
@filter_params = filter_params
-
end
-
-
4
def call
-
50
filter_records.page(filter_params[:page] || 1)
-
end
-
-
4
private
-
-
4
attr_reader :model_data_scope, :filter_params
-
-
4
def text_search(records, text_search)
-
23
return records if text_search.blank?
-
-
7
records.search(text_search)
-
end
-
-
4
def course_search(records, search)
-
27
return records if search.blank?
-
-
7
records.course_search(search)
-
end
-
-
4
def provider_search(records, search)
-
27
return records if search.blank?
-
-
9
records.provider_search(search)
-
end
-
-
4
def user_type(records, user_type_arr)
-
16
return records if user_type_arr&.all?(&:blank?)
-
-
16
case user_type_arr
-
when ["admin"]
-
records.admins
-
when ["provider"]
-
2
records.non_admins
-
else
-
14
records
-
end
-
end
-
-
4
def filter_records
-
50
filtered_records = model_data_scope
-
-
50
filtered_records = text_search(filtered_records, filter_params[:text_search]) if filtered_records.respond_to?(:search)
-
50
filtered_records = provider_search(filtered_records, filter_params[:provider_search]) if filtered_records.respond_to?(:provider_search)
-
50
filtered_records = course_search(filtered_records, filter_params[:course_search]) if filtered_records.respond_to?(:course_search)
-
50
filtered_records = user_type(filtered_records, filter_params[:user_type]) if filtered_records.respond_to?(:admins)
-
-
50
filtered_records
-
end
-
end
-
end
-
2
module Token
-
2
class DecodeService
-
2
include ServicePattern
-
-
2
def initialize(encoded_token:,
-
secret:,
-
algorithm:,
-
audience:,
-
issuer:,
-
subject:)
-
-
30
@encoded_token = encoded_token
-
-
30
@secret = secret
-
30
@algorithm = algorithm
-
30
@audience = audience
-
30
@issuer = issuer
-
30
@subject = subject
-
end
-
-
2
def call
-
30
decoded_token = JWT.decode(
-
encoded_token,
-
secret,
-
true,
-
{
-
algorithm: algorithm,
-
verify_iss: true,
-
verify_aud: true,
-
verify_sub: true,
-
verify_iat: true,
-
exp_leeway: 6.seconds.to_i,
-
**claims,
-
},
-
)
-
-
24
(payload, _algorithm) = decoded_token
-
-
24
payload.with_indifferent_access[:data]
-
end
-
-
2
private
-
-
2
attr_reader :encoded_token, :secret, :algorithm, :audience, :issuer, :subject
-
-
2
def claims
-
30
@claims ||= {
-
aud: audience,
-
iss: issuer,
-
sub: subject,
-
}
-
end
-
end
-
end
-
1
module UserAssociationsService
-
1
class Create
-
1
attr_reader :provider, :user, :all_providers
-
-
1
class << self
-
1
def call(**args)
-
8
new(args).call
-
end
-
end
-
-
1
def initialize(user:, provider: nil, all_providers: false)
-
8
@provider = provider
-
8
@user = user
-
8
@all_providers = all_providers
-
end
-
-
1
def call
-
8
add_user_to_providers
-
8
update_user_notification_preferences
-
end
-
-
1
private_class_method :new
-
-
1
private
-
-
1
def add_user_to_providers
-
8
if all_providers
-
4
add_user_to_all_providers
-
else
-
4
add_user_to_a_single_provider
-
end
-
end
-
-
1
def add_user_to_a_single_provider
-
4
provider.users << user
-
end
-
-
1
def add_user_to_all_providers
-
4
user.providers = Provider.all
-
end
-
-
1
def update_user_notification_preferences
-
8
user_notification_preferences = UserNotificationPreferences.new(user_id: user.id)
-
8
return if user_notification_preferences.updated_at.nil?
-
-
4
user_notification_preferences.update(
-
enable_notifications: user_notification_preferences.enabled,
-
)
-
end
-
end
-
end
-
1
module UserAssociationsService
-
1
class Delete
-
1
attr_reader :user, :providers
-
-
1
class << self
-
1
def call(**args)
-
6
new(args).call
-
end
-
end
-
-
1
def initialize(user:, providers:)
-
6
@providers = providers
-
6
@user = user
-
end
-
-
1
def call
-
6
remove_access
-
6
update_user_notification_preferences
-
end
-
-
1
private_class_method :new
-
-
1
private
-
-
1
def remove_access
-
6
user.remove_access_to(providers)
-
end
-
-
1
def update_user_notification_preferences
-
6
user_notification_preferences = UserNotificationPreferences.new(user_id: user.id)
-
6
return if user_notification_preferences.updated_at.nil?
-
-
4
user_notification_preferences.update(
-
enable_notifications: user_notification_preferences.enabled,
-
)
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
4
module UserSessions
-
4
class Update
-
4
include ServicePattern
-
-
4
attr_reader :user, :successful
-
-
4
alias_method :successful?, :successful
-
-
4
def initialize(user:, user_session:)
-
227
@user = user
-
-
attributes = {
-
227
last_login_date_utc: Time.zone.now,
-
email: user_session.email,
-
sign_in_user_id: user_session.sign_in_user_id,
-
}
-
-
227
attributes[:first_name] = user_session.first_name if user_session.first_name.present?
-
227
attributes[:last_name] = user_session.last_name if user_session.last_name.present?
-
-
227
@user.assign_attributes(attributes)
-
end
-
-
4
def call
-
231
@successful = user.valid? && user.save!
-
-
231
self
-
end
-
end
-
end
-
3
module V3
-
3
class CourseSearchService
-
3
def initialize(filter: nil, sort: nil, course_scope: Course)
-
96
@filter = filter || {}
-
96
@sort = Set.new(sort&.split(","))
-
96
@course_scope = course_scope
-
end
-
-
3
class << self
-
3
def call(**args)
-
96
new(args).call
-
end
-
end
-
-
3
def call
-
96
scope = course_scope
-
96
scope = scope.with_salary if funding_filter_salary?
-
96
scope = scope.with_qualifications(qualifications) if qualifications.any?
-
96
scope = scope.with_vacancies if with_vacancies?
-
96
scope = scope.findable if findable?
-
96
scope = scope.with_study_modes(study_types) if study_types.any?
-
96
scope = scope.with_subjects(subject_codes) if subject_codes.any?
-
96
scope = scope.with_provider_name(provider_name) if provider_name.present?
-
96
scope = scope.with_send if send_courses_filter?
-
96
scope = scope.within(filter[:radius], origin: origin) if locations_filter?
-
96
scope = scope.with_funding_types(funding_types) if funding_types.any?
-
96
scope = scope.with_degree_grades(degree_grades) if degree_grades.any?
-
96
scope = scope.provider_can_sponsor_visa if can_sponsor_visa_filter?
-
-
96
scope = scope.includes(
-
:enrichments,
-
:financial_incentives,
-
course_subjects: [:subject],
-
site_statuses: [:site],
-
provider: %i[recruitment_cycle ucas_preferences],
-
)
-
-
96
if provider_name.present?
-
5
scope = scope
-
.joins(:provider).order("by_provider_name")
-
.ascending_canonical_order
-
5
scope = scope.select("DISTINCT(course.id), course.*, provider.provider_name, CASE WHEN provider.provider_name = #{ActiveRecord::Base.connection.quote(provider_name)} THEN '1' END as by_provider_name")
-
91
elsif sort_by_provider_ascending?
-
3
scope = scope.ascending_canonical_order
-
3
scope = scope.select("DISTINCT(course.id), course.*, provider.provider_name")
-
88
elsif sort_by_provider_descending?
-
2
scope = scope.descending_canonical_order
-
2
scope = scope.select("DISTINCT(course.id), course.*, provider.provider_name")
-
86
elsif sort_by_distance?
-
13
scope = scope.joins(courses_with_distance_from_origin)
-
13
scope = scope.joins(:provider)
-
13
scope = scope.select("DISTINCT(course.id), course.*, distance, #{Course.sanitize_sql(distance_with_university_area_adjustment)}")
-
-
scope =
-
13
if expand_university?
-
1
scope.order(:boosted_distance)
-
else
-
12
scope.order(:distance)
-
end
-
73
elsif !scope.to_sql.include?("DISTINCT")
-
73
scope = scope.select("DISTINCT(course.id), course.*")
-
end
-
-
96
scope
-
end
-
-
3
private_class_method :new
-
-
3
private
-
-
3
def expand_university?
-
13
filter[:expand_university].to_s.downcase == "true"
-
end
-
-
3
def distance_with_university_area_adjustment
-
13
university_provider_type = Provider.provider_types[:university]
-
13
university_location_area_radius = 10
-
13
<<~EOSQL.gsub(/\s+/m, " ").strip
-
(CASE
-
WHEN provider.provider_type = '#{university_provider_type}'
-
THEN (distance - #{university_location_area_radius})
-
ELSE distance
-
END) as boosted_distance
-
EOSQL
-
end
-
-
3
def locatable_sites
-
26
site_statuses = SiteStatus.arel_table
-
26
sites = Site.arel_table
-
-
# Create virtual table with sites and site statuses
-
26
site_statuses.join(sites).on(site_statuses[:site_id].eq(sites[:id]))
-
.where(site_statuses_criteria(site_statuses))
-
.where(already_geocoded_criteria(sites))
-
.where(locatable_address_criteria(sites))
-
end
-
-
3
def site_statuses_criteria(site_statuses)
-
# Only running and published site statuses
-
26
running_and_published_criteria = site_statuses[:status].eq(SiteStatus.statuses[:running]).and(site_statuses[:publish].eq(SiteStatus.publishes[:published]))
-
-
26
if with_vacancies?
-
# Only site statuses with vacancies
-
2
running_and_published_criteria
-
.and(site_statuses[:vac_status])
-
.eq_any([
-
SiteStatus.vac_statuses[:full_time_vacancies],
-
SiteStatus.vac_statuses[:part_time_vacancies],
-
SiteStatus.vac_statuses[:both_full_time_and_part_time_vacancies],
-
])
-
else
-
24
running_and_published_criteria
-
end
-
end
-
-
3
def already_geocoded_criteria(sites)
-
# we only want sites that have been geocoded
-
26
sites[:latitude].not_eq(nil).and(sites[:longitude].not_eq(nil))
-
end
-
-
3
def locatable_address_criteria(sites)
-
# only sites that have a locatable address
-
# there are some sites with no address1 or postcode that cannot be
-
# accurately geocoded. We don't want to return these as the closest site.
-
# This should be removed once the data is fixed
-
26
sites[:address1].not_eq("").or(sites[:postcode].not_eq(""))
-
end
-
-
3
def course_id_with_lowest_locatable_distance
-
# select course_id and nearest site with shortest distance from origin
-
# as courses may have multiple sites
-
# this will remove duplicates by aggregating on course_id
-
26
origin_lat_long = Struct.new(:lat, :lng).new(origin[0].to_f, origin[1].to_f)
-
26
lowest_locatable_distance = Arel.sql("MIN#{Site.sanitize_sql(Site.distance_sql(origin_lat_long))} as distance")
-
26
locatable_sites.project(:course_id, lowest_locatable_distance).group(:course_id)
-
end
-
-
3
def distance_table
-
# form a temporary table with results
-
26
Arel::Nodes::TableAlias.new(
-
Arel.sql(
-
format("(%s)", course_id_with_lowest_locatable_distance.to_sql),
-
), "distances"
-
)
-
end
-
-
3
def courses_with_distance_from_origin
-
# grab courses table and join with the above result set
-
# so distances from origin are now available
-
# we can then sort by distance from the given origin
-
13
courses_table = Course.arel_table
-
13
courses_table.join(distance_table).on(courses_table[:id].eq(distance_table[:course_id])).join_sources
-
end
-
-
3
def locations_filter?
-
96
filter.key?(:latitude) &&
-
filter.key?(:longitude) &&
-
filter.key?(:radius)
-
end
-
-
3
def sort_by_provider_ascending?
-
91
sort == Set["name", "provider.provider_name"]
-
end
-
-
3
def sort_by_provider_descending?
-
88
sort == Set["-name", "-provider.provider_name"]
-
end
-
-
3
def sort_by_distance?
-
86
sort == Set["distance"]
-
end
-
-
3
def origin
-
54
[filter[:latitude], filter[:longitude]]
-
end
-
-
3
attr_reader :sort, :filter, :course_scope
-
-
3
def funding_filter_salary?
-
96
filter[:funding] == "salary"
-
end
-
-
3
def qualifications
-
99
return [] if filter[:qualification].blank?
-
-
6
filter[:qualification].split(",")
-
end
-
-
3
def with_vacancies?
-
122
filter[:has_vacancies].to_s.downcase == "true"
-
end
-
-
3
def findable?
-
96
filter[:findable].to_s.downcase == "true"
-
end
-
-
3
def study_types
-
102
return [] if filter[:study_type].blank?
-
-
12
filter[:study_type].split(",")
-
end
-
-
3
def funding_types
-
100
return [] if filter[:funding_type].blank?
-
-
8
filter[:funding_type].split(",")
-
end
-
-
3
def degree_grades
-
101
return [] if filter[:degree_grade].blank?
-
-
10
filter[:degree_grade].split(",")
-
end
-
-
3
def subject_codes
-
102
return [] if filter[:subjects].blank?
-
-
12
filter[:subjects].split(",")
-
end
-
-
3
def provider_name
-
202
return [] if filter[:"provider.provider_name"].blank?
-
-
20
filter[:"provider.provider_name"]
-
end
-
-
3
def send_courses_filter?
-
96
filter[:send_courses].to_s.downcase == "true"
-
end
-
-
3
def can_sponsor_visa_filter?
-
96
filter[:can_sponsor_visa].to_s.downcase == "true"
-
end
-
end
-
end
-
2
class VacancyStatusDeterminationService
-
2
attr_reader :vacancy_status_full_time,
-
:vacancy_status_part_time,
-
:course
-
-
2
def self.call(vacancy_status_full_time:, vacancy_status_part_time:, course:)
-
28
new(
-
vacancy_status_full_time: vacancy_status_full_time,
-
vacancy_status_part_time: vacancy_status_part_time,
-
course: course,
-
).vacancy_status
-
end
-
-
2
def initialize(vacancy_status_full_time:, vacancy_status_part_time:, course:)
-
28
@vacancy_status_full_time = vacancy_status_full_time
-
28
@vacancy_status_part_time = vacancy_status_part_time
-
28
@course = course
-
end
-
-
2
def vacancy_status
-
28
return "both_full_time_and_part_time_vacancies" if full_or_part_time?
-
25
return "full_time_vacancies" if full_time?
-
15
return "part_time_vacancies" if part_time?
-
-
13
"no_vacancies"
-
end
-
-
2
private
-
-
2
def part_time?
-
15
(course.full_time_or_part_time? && vacancy_status_part_time?) ||
-
14
(course.part_time? && vacancy_status_part_time?)
-
end
-
-
2
def full_time?
-
25
(course.full_time_or_part_time? && vacancy_status_full_time?) ||
-
24
(course.full_time? && vacancy_status_full_time?)
-
end
-
-
2
def full_or_part_time?
-
28
course.full_time_or_part_time? &&
-
6
(vacancy_status_full_time? && vacancy_status_part_time?)
-
end
-
-
2
def vacancy_status_full_time?
-
29
vacancy_status_full_time == "1"
-
end
-
-
2
def vacancy_status_part_time?
-
8
vacancy_status_part_time == "1"
-
end
-
end
-
4
class EmailAddressValidator < ActiveModel::EachValidator
-
4
EMAIL_VALIDATION_ERROR_MESSAGE = "^Enter an email address in the correct format, like name@example.com".freeze
-
-
4
def validate_each(record, attribute, value)
-
3456
unless value =~ /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i
-
27
record.errors.add attribute, (options[:message] || EMAIL_VALIDATION_ERROR_MESSAGE)
-
end
-
end
-
end
-
4
class PhoneValidator < ActiveModel::EachValidator
-
4
PHONE_VALIDATION_ERROR_MESSAGE = "^Enter a valid telephone number".freeze
-
-
4
def validate_each(record, attribute, value)
-
3465
if value.blank? || is_invalid_phone_number_format?(value)
-
13
record.errors.add(attribute, message: options[:message] || PHONE_VALIDATION_ERROR_MESSAGE)
-
end
-
end
-
-
4
private
-
-
4
def is_invalid_phone_number_format?(value)
-
3456
value.match?(/[^ext()+\. 0-9]/)
-
end
-
end
-
4
class PostcodeValidator < ActiveModel::EachValidator
-
4
def validate_each(record, attribute, value)
-
833
return unless value
-
-
831
postcode = UKPostcode.parse(value)
-
831
unless postcode.full_valid?
-
3
record.errors.add(attribute, message: options[:message] || "is not valid (for example, BN1 1AA)")
-
end
-
end
-
end
-
4
class ReferenceNumberFormatValidator < ActiveModel::EachValidator
-
4
def validate_each(record, attribute, value)
-
7745
return if options[:allow_blank] && value.blank?
-
-
7745
if value.blank? || value.length > options[:maximum] || value.length < options[:minimum] || !value.match?(/\A-?\d+\Z/)
-
9
record.errors.add attribute, options[:message]
-
end
-
end
-
end
-
4
class UniqueCourseValidator < ActiveModel::Validator
-
4
def validate(record)
-
171
return if course_is_unique?(record)
-
-
3
record.errors.add(:base, :duplicate)
-
end
-
-
4
private
-
-
4
def course_is_unique?(new_course)
-
171
existing_courses = new_course.provider.courses
-
171
new_course_attributes = new_course.attributes.slice(*attributes_to_compare)
-
171
existing_courses.none? do |existing_course|
-
28
existing_course_attributes = existing_course.attributes.slice(*attributes_to_compare)
-
-
28
new_course_attributes == existing_course_attributes &&
-
new_course.subjects == existing_course.subjects &&
-
new_course.accrediting_provider == existing_course.accrediting_provider
-
end
-
end
-
-
4
def attributes_to_compare
-
199
%w[level is_send age_range_in_years qualification program_type study_mode maths english science applications_open_from start_date]
-
end
-
end
-
4
class WordsCountValidator < ActiveModel::EachValidator
-
4
def validate_each(record, attribute, string)
-
14513
return if string.blank?
-
-
14216
if word_count(string) > options[:maximum]
-
37
record.errors.add(
-
attribute,
-
message: options[:message] || "^Reduce the word count for #{attribute.to_s.humanize(capitalize: false)}",
-
)
-
end
-
end
-
-
4
def word_count(string)
-
14216
string.scan(/\S+/).size
-
end
-
end
-
3
class AllocationsView
-
3
include ActionView::Helpers::TextHelper
-
-
3
module Status
-
3
REQUESTED = "REQUESTED".freeze
-
3
NOT_REQUESTED = "NOT REQUESTED".freeze
-
3
YET_TO_REQUEST = "YET TO REQUEST".freeze
-
3
NO_REQUEST_SENT = "NO REQUEST SENT".freeze
-
end
-
-
3
module Colour
-
3
GREEN = "green".freeze
-
3
RED = "red".freeze
-
3
GREY = "grey".freeze
-
end
-
-
3
module Requested
-
3
YES = "yes".freeze
-
3
NO = "no".freeze
-
end
-
-
3
module RequestType
-
3
INITIAL = "initial".freeze
-
3
REPEAT = "repeat".freeze
-
3
DECLINED = "declined".freeze
-
end
-
-
3
def initialize(training_providers:, allocations:)
-
50
@training_providers = training_providers
-
50
@allocations = allocations
-
end
-
-
3
def repeat_allocation_statuses
-
10
filtered_training_providers.map do |training_provider|
-
11
matching_allocation = find_matching_allocation(training_provider, repeat_allocations)
-
11
build_repeat_allocations(matching_allocation, training_provider)
-
end
-
end
-
-
3
def initial_allocation_statuses
-
36
statuses = initial_allocations.map do |allocation|
-
18
build_initial_allocations(allocation, allocation.provider)
-
end
-
-
36
statuses.compact
-
end
-
-
3
def requested_allocations_statuses
-
15
statuses = requested_allocations.map do |allocation|
-
10
build_requested_allocations(allocation, allocation.provider)
-
end
-
-
25
statuses.compact.sort_by! { |hsh| hsh[:training_provider_name] }
-
end
-
-
3
def confirmed_allocation_places
-
6
statuses = confirmed_allocations.map do |allocation|
-
4
build_confirmed_allocations(allocation, allocation.provider)
-
end
-
-
10
statuses.compact.sort_by! { |hsh| hsh[:training_provider_name] }
-
end
-
-
3
def not_requested_allocations_statuses
-
7
statuses = filtered_training_providers.map do |training_provider|
-
5
matching_allocation = find_matching_allocation(training_provider, not_requested_allocations)
-
5
build_not_requested_allocations(matching_allocation, training_provider)
-
end
-
-
11
statuses.compact.sort_by! { |hsh| hsh[:training_provider_name] }
-
end
-
-
3
private
-
-
3
def filtered_training_providers
-
# When displaying 'repeat allocation statuses'
-
# we need to first filter out those training providers
-
# who will be allocated places for the first time (i.e. where the accredited provider)
-
# has made initial allocation requests on their behalf)
-
21
training_provider_provider_codes = initial_allocations.map { |allocation| allocation.provider.provider_code }
-
36
@training_providers.reject { |tp| training_provider_provider_codes.include?(tp.provider_code) }
-
end
-
-
3
def repeat_allocations
-
20
@allocations.reject { |allocation| allocation.request_type == RequestType::INITIAL }
-
end
-
-
3
def initial_allocations
-
91
@allocations.select { |allocation| allocation.request_type == RequestType::INITIAL }
-
end
-
-
3
def requested_allocations
-
27
@allocations.select { |allocation| allocation.request_type.in?([RequestType::INITIAL, RequestType::REPEAT]) }
-
end
-
-
3
def confirmed_allocations
-
11
@allocations.select { |allocation| allocation.request_type.in?([RequestType::INITIAL, RequestType::REPEAT]) }
-
end
-
-
3
def not_requested_allocations
-
7
@allocations.select { |allocation| allocation.request_type == RequestType::DECLINED }
-
end
-
-
3
def find_matching_allocation(training_provider, allocations)
-
23
allocations.find { |allocation| allocation.provider.provider_code == training_provider.provider_code }
-
end
-
-
3
def build_repeat_allocations(matching_allocation, training_provider)
-
allocation_status = {
-
11
training_provider_name: training_provider.provider_name,
-
training_provider_code: training_provider.provider_code,
-
}
-
-
11
if yet_to_request?(matching_allocation)
-
5
allocation_status[:status] = Status::YET_TO_REQUEST
-
5
allocation_status[:status_colour] = Colour::GREY
-
end
-
-
11
if requested?(matching_allocation)
-
4
allocation_status[:status] = Status::REQUESTED
-
4
allocation_status[:status_colour] = Colour::GREEN
-
4
allocation_status[:requested] = Requested::YES
-
end
-
-
11
if not_requested?(matching_allocation)
-
2
allocation_status[:status] = Status::NOT_REQUESTED
-
2
allocation_status[:status_colour] = Colour::RED
-
2
allocation_status[:requested] = Requested::NO
-
end
-
-
11
if matching_allocation
-
6
allocation_status[:id] = matching_allocation.id
-
6
allocation_status[:request_type] = matching_allocation.request_type
-
end
-
-
11
if matching_allocation&.accredited_body
-
6
allocation_status[:accredited_body_code] = matching_allocation.accredited_body.provider_code
-
end
-
-
11
allocation_status
-
end
-
-
3
def build_initial_allocations(matching_allocation, training_provider)
-
18
return if matching_allocation.nil?
-
-
hash = {
-
18
training_provider_name: training_provider.provider_name,
-
training_provider_code: training_provider.provider_code,
-
status_colour: Colour::GREEN,
-
requested: Requested::YES,
-
request_type: matching_allocation.request_type,
-
status: "#{pluralize(matching_allocation.number_of_places, 'place')} requested".upcase,
-
}
-
-
18
hash[:id] = matching_allocation.id if matching_allocation.id
-
-
18
hash
-
end
-
-
3
def build_confirmed_allocations(allocation, training_provider)
-
4
return if allocation.nil?
-
-
{
-
4
training_provider_name: training_provider.provider_name,
-
number_of_places: allocation.number_of_places,
-
confirmed_number_of_places: allocation.confirmed_number_of_places,
-
uplifts: allocation.allocation_uplift&.uplifts,
-
total: allocation.confirmed_number_of_places.to_i + allocation.allocation_uplift&.uplifts.to_i,
-
}
-
end
-
-
3
def build_requested_allocations(allocation, training_provider)
-
10
return if allocation.nil?
-
-
{
-
10
training_provider_name: training_provider.provider_name,
-
training_provider_code: training_provider.provider_code,
-
status_colour: Colour::GREEN,
-
status: Status::REQUESTED,
-
}
-
end
-
-
3
def build_not_requested_allocations(allocation, training_provider)
-
6
return if training_provider.provider_name.in?(requested_allocations_statuses.map { |tp| tp[:training_provider_name] })
-
-
allocation_status = {
-
4
training_provider_name: training_provider.provider_name,
-
training_provider_code: training_provider.provider_code,
-
}
-
-
4
if yet_to_request?(allocation)
-
3
allocation_status[:status] = "NO REQUEST SENT"
-
3
allocation_status[:status_colour] = Colour::GREY
-
end
-
-
4
if not_requested?(allocation)
-
1
allocation_status[:status] = Status::NOT_REQUESTED
-
1
allocation_status[:status_colour] = Colour::RED
-
end
-
-
4
allocation_status
-
end
-
-
3
def not_requested?(matching_allocation)
-
15
matching_allocation && matching_allocation[:request_type] == RequestType::DECLINED
-
end
-
-
3
def requested?(matching_allocation)
-
11
matching_allocation && matching_allocation[:request_type] == RequestType::REPEAT
-
end
-
-
3
def yet_to_request?(matching_allocation)
-
15
matching_allocation.nil?
-
end
-
end
-
1
class NotificationsView
-
1
ORGANISATION_URL_PATTERN = /\/organisations\/(\S+)\/?/.freeze
-
-
1
def initialize(
-
request:,
-
current_user:
-
)
-
9
@request = request
-
9
@current_user = current_user
-
end
-
-
1
def user_id
-
1
current_user.id
-
end
-
-
1
def user_email
-
1
current_user.email
-
end
-
-
1
def back_link_path
-
5
return Rails.application.routes.url_helpers.root_path if ORGANISATION_URL_PATTERN.match(request.referer).nil?
-
-
3
URI(request.referer).path
-
end
-
-
1
def provider_code
-
5
matches = ORGANISATION_URL_PATTERN.match(request.referer)
-
5
return if matches.nil?
-
-
3
matches[1]
-
end
-
-
1
private
-
-
1
attr_reader :request, :current_user
-
end
-
module Rack
-
class RequestOutput
-
def initialize(app)
-
@app = app
-
end
-
-
def call(env)
-
Rails.logger.info { "API HIT => #{env['rack.url_scheme']}://#{env['HTTP_HOST']}#{env['REQUEST_URI']}" }
-
-
@app.call(env)
-
end
-
end
-
end